Vue 编译器原理深入
模板编译全流程
1. 编译三阶段
模板字符串
-> Parse(词法+语法分析)-> AST(抽象语法树)
-> Transform(优化转换)-> 带 PatchFlag 的 JavaScript AST
-> Codegen(代码生成)-> render 函数字符串
2. Parse 阶段
- 有限状态机解析 HTML:Data -> Tag Open -> Tag Name -> Attribute
- 生成元素节点、文本节点、插值节点
// <div :class="cls">{{ msg }}</div> 解析后
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{ name: 'class', exp: { content: 'cls' } }],
children: [{
type: NodeTypes.INTERPOLATION,
content: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'msg' }
}]
}
3. Transform 阶段(核心优化)
静态提升(Static Hoisting)
// 模板:
// <ul>
// <li class="static">固定内容</li>
// <li>{{ item }}</li>
// </ul>
// 静态节点提升到 render 函数外
const _hoisted_1 = createVNode('li', { class: 'static' }, '固定内容')
function render() {
return createVNode('ul', null, [
_hoisted_1, // 直接复用,不重新创建
createVNode('li', null, ctx.item, PatchFlags.TEXT)
])
}
PatchFlag 靶向更新
// 标记节点哪部分是动态的
createVNode('div',
{ class: dynamicClass },
null,
PatchFlags.CLASS // 只有 class 是动态的,diff 时只比较 class
)
常见 PatchFlag 值:
TEXT = 1:动态文本CLASS = 2:动态 classSTYLE = 4:动态 stylePROPS = 8:动态非 class/style 属性CACHED = -1:静态节点,跳过所有 diff
Block Tree
- 每个含动态节点的模板区域是一个 Block
- Block 记录其内部所有动态子节点(dynamicChildren)
- Diff 时只遍历 dynamicChildren,跳过静态节点
v-if/v-for会创建新 Block,因为结构会变化
4. v-if / v-for 编译
// v-if 编译为三元表达式
// <div v-if="show">A</div><div v-else>B</div>
_ctx.show ? createVNode('div', null, 'A') : createVNode('div', null, 'B')
// v-for 编译为 renderList
// <li v-for="item in list" :key="item.id">{{ item.name }}</li>
renderList(_ctx.list, (item) =>
createVNode('li', { key: item.id }, toDisplayString(item.name), 1)
)
// v-model 编译为双向绑定
// <input v-model="val">
createVNode('input', {
value: _ctx.val,
onInput: $event => { _ctx.val = $event.target.value }
})
5. 事件缓存
// <button @click="handleClick">按钮</button>
// 编译后:handler 被缓存,避免每次渲染创建新函数
createVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
})
// 内联 handler 会被包裹后缓存
6. 编译时 vs 运行时
| 版本 | 说明 | Bundle 大小 |
|---|---|---|
vue.runtime.esm | 仅运行时,无编译器 | 小(推荐,Vite/Webpack 用) |
vue.esm-bundler | 运行时 + 编译器 | 大(CDN 直接使用时) |
- 使用构建工具时:模板在构建阶段编译,运行时不需要编译器
- 直接
<script src>时:需要运行时编译器
7. 资深面试题
- Vue 3 编译优化为什么比 Vue 2 快?
- 静态提升:不变的节点不重复创建
- PatchFlag:diff 只比较变化的部分
- Block Tree:O(全树) -> O(动态节点数)
- 事件缓存:handler 不每次重新创建
- 为什么 v-if 和 v-for 不能同时使用?
- Vue 3 中 v-if 优先级高于 v-for,v-if 内无法访问 v-for 的变量
- 解决:外层包一个
<template v-for>或用 computed 过滤数据
- 自定义指令如何在编译层面优化?
- 通过
directiveTransforms在 Transform 阶段处理 - 可以将部分指令逻辑转为静态属性,避免运行时指令调用开销
- 通过
- 模板中
key的作用是什么?编译层面如何处理?- 用于标识节点身份,相同 key 的节点会尝试复用
- 编译时 key 被提取为 VNode 的第二个参数
v-for中动态 key 会生成 KEYED_FRAGMENT,告知 diff 算法使用带 key 的策略