跳到主要内容

Vue 编译器原理深入

模板编译全流程

1. 编译三阶段

模板字符串
-> Parse(词法+语法分析)-> AST(抽象语法树)
-> Transform(优化转换)->PatchFlagJavaScript 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:动态 class
  • STYLE = 4:动态 style
  • PROPS = 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 快?
    1. 静态提升:不变的节点不重复创建
    2. PatchFlag:diff 只比较变化的部分
    3. Block Tree:O(全树) -> O(动态节点数)
    4. 事件缓存: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 的策略