Skip to main content
我对 Vue 编译器的理解分几次补上的:先记住模板会变成 render 函数,后来才弄清 PatchFlag 和 Block Tree 怎么把 diff 缩小到动态节点。

Q1: 模板编译的三个阶段是什么?

1

阶段一: Parse(解析)

编译器把模板字符串解析成 AST(抽象语法树)。每个节点记录了标签名、属性、子节点、指令等信息。比如 <div :class="dynamic">{{ msg }}</div> 会被解析成一个 Element 节点,包含一个 Directive 属性和一个 Interpolation 子节点。
2

阶段二: Transform(转换)

遍历 AST,对节点做优化标记。这是编译器最核心的阶段——PatchFlag、Block Tree、静态提升、事件缓存等优化都在这个阶段完成。转换后的 AST 包含了运行时需要的所有优化信息。
3

阶段三: Generate(代码生成)

把优化后的 AST 转换成 JavaScript 代码字符串。就是你看到的 createElementVNode("div", null, ...) 这种代码。生成的代码包含静态提升的变量声明、render 函数体、以及所有 PatchFlag 标记。
编译器解决的核心问题:
编译器做三件事:解析(模板字符串 → AST)、转换(AST 优化标记)、生成(AST → 渲染函数代码)。核心价值在转换阶段的优化标记。

没有编译器优化

每次渲染都遍历整棵 VNode 树,比较所有属性。就像每次盘点仓库都要逐个检查所有货架上的所有商品——即使 99% 的商品根本没动过。

有编译器优化

编译器在构建时标记哪些节点是动态的,运行时只遍历标记的节点。就像仓库管理系统在商品入库时就打上标签——盘点时只检查有标签的商品,跳过所有没动过的。
回答时重点说编译器做了什么优化(PatchFlag、静态提升、Block Tree),不要只说”模板变成渲染函数”——想听的是你对为什么模板比手写 render 函数性能好的理解。

Q2: PatchFlag 是怎么工作的?

位掩码的基本原理

PatchFlag 是编译器在 createElementVNode 调用中附加的位掩码数字,编码了元素有哪些动态绑定。运行时通过位运算快速判断需要更新哪些属性,跳过所有静态属性的比较。

位运算的执行过程

编译器用按位或(|)组合多个标记,运行时用按位与(&)检查:
React 的 JSX 在构建时由 Babel 转换,但产物仍然是运行时的 createElement/jsx 调用,构建工具不会做模板级的静态分析,因此运行时需要通过比较新旧 props 来判断哪些属性变化了。Vue 3 的模板是编译时 DSL,编译器能在构建阶段确定动态绑定,这是 PatchFlag 与 Block Tree 能成立的前提。

常见的 PatchFlag 值


Q3: Block Tree 的动态节点追踪是怎么实现的?

Block 的核心思想

模板中只有 v-if/v-for 会改变 DOM 结构,其他区域的节点数量和层级是固定的。Block Tree 的核心思想是:把结构稳定的区域标记为 Block,内部维护一个扁平化的 dynamicChildren 数组,重渲染时只遍历这个数组,静态子树完全跳过。

openBlock + createBlock 的实现

运行时 diff 的优化效果

重渲染时,如果一个 VNode 有 dynamicChildren,渲染器直接遍历这个数组而不是 children:
v-if 和 v-for 会改变 DOM 结构,所以它们会创建自己的子 Block,打断父 Block 的 dynamicChildren 收集。深层嵌套的条件渲染会削弱这个优化效果。

Q4: 静态提升是怎么实现的?

基本原理

不依赖响应式数据的 VNode 被提升到渲染函数外部,只创建一次,后续渲染直接复用引用。

事件缓存

除了 VNode 静态提升,Vue 3 的编译器还会缓存事件处理函数的引用:
事件缓存确保内联函数不每次创建新的引用,避免子组件因为 props 变化(函数引用变化)而触发不必要的 re-render。
静态提升让不依赖响应式的节点只创建一次,事件缓存让内联函数引用不变。两者共同减少了 VNode 创建和 diff 的开销。

Q5: 编译器是怎么处理 v-if 和 v-for 的?

v-if 的编译

v-if 在编译器中被转换为三元表达式。每个分支都是一个独立的 Block:

v-for 的编译

v-for 编译为 renderList 函数调用,每一项创建一个独立的 VNode:

v-if 和 v-for 对 Block Tree 的影响

v-if/v-for 会创建子 Block,打断父 Block 的 dynamicChildren 收集。这意味着:
避免在同一个元素上同时使用 v-for 和 v-if。Vue 3 中 v-if 的优先级高于 v-for,此时 v-if 取不到 v-for 的作用域变量(Vue 2 则相反,v-for 优先,每次渲染都会先遍历再判断)。应该用 computed 预先过滤列表,或者把 v-if 移到外层容器上。

Q6: 编译器是怎么处理 v-model 的?

v-model 在编译器中根据元素类型生成不同的代码:

原生元素

组件

带参数的 v-model

v-model 在原生元素上会编译为 value + onInput 两个动态属性;在组件上则是 modelValue + onUpdate:modelValue。带参数的多个 v-model 会继续叠加动态属性,需要比较的 props 随之增多。在性能敏感的场景,可以考虑用 :value + @input 手动绑定,把更新范围收窄。

Q7: 编译器的优化对 SSR 有什么影响?

SSR 的编译优化

Vue 3 的编译器为 SSR 生成了专门的代码路径。静态节点在 SSR 中直接输出 HTML 字符串,不需要创建 VNode:

静态节点的 SSR 优化

编译器会标记完全静态的子树,SSR 渲染时直接输出预编译的 HTML 字符串:
编译器的静态分析在 SSR 中价值更大——静态节点直接输出 HTML 字符串,连 VNode 都不需要创建,SSR 性能大幅提升。

Q8: Vue 的编译器和 Svelte 的编译器有什么区别?

设计理念的差异

Svelte 的编译策略

Svelte 的编译器在编译时分析每个变量在模板中的使用位置,为每个变量生成精确的 DOM 更新代码:

Vue 3 的编译策略

Vue 3 的编译器生成的是渲染函数,运行时通过 diff 找出差异:
Svelte 没有虚拟 DOM 的开销——不需要创建 VNode、不需要 diff、不需要遍历。但 Vue 3 的虚拟 DOM 也有优势:跨平台能力(SSR、Weex、Canvas)、更好的 TypeScript 类型推断、以及 Block Tree 优化后接近 Svelte 的性能。选择取决于项目需求。

底层源码:编译器的核心实现

Vue 3 的编译器入口在 packages/compiler-core/src/compile.ts(详见 Vue 3 源码 packages/compiler-core/src/compile.ts):
PatchFlag 在 transformElement 转换中生成。当遍历到元素节点时,编译器检查节点的所有属性和子节点,确定哪些是动态的,然后计算 PatchFlag 值(详见 Vue 3 源码 packages/compiler-core/src/transforms/transformElement.ts):
Block Tree 由编译期与运行时配合完成:transformElement 负责计算 PatchFlag,codegen 阶段把结构稳定的节点包装成 openBlock() + createElementBlock();运行时 openBlock/createBlock(packages/runtime-core/src/vnode.ts)在创建 Block 时把带 PatchFlag 的子节点收集进 dynamicChildren。hoistStatic 负责的是静态提升与静态节点缓存,不负责 Block 的建立。

十、及格回答 vs 满分回答

问题:“Vue 3 的编译器做了哪些优化?”

及格回答(60-70分):
“编译器做了 PatchFlag、Block Tree、静态提升三个优化。PatchFlag 标记动态绑定,Block Tree 扁平化动态节点,静态提升让静态节点只创建一次。”
问题出在:知道优化名称但不理解实现。 满分回答(90-100分):
Vue 3 的编译器在 baseCompile 函数中分三阶段工作:parse(模板 → AST)、transform(AST 优化标记)、generate(AST → 渲染函数代码)。核心价值在 transform 阶段的三层优化。[1]
PatchFlag:在 transformElement 中生成。编译器遍历元素的所有属性和子节点,用位掩码标记动态绑定类型(TEXT=1、CLASS=2、STYLE=4、PROPS=8)。运行时用位运算检查,只更新标记的部分。React 没有等价的模板级标记,只能在运行时比较新旧 props。[2]
Block Tree:由 codegen 生成的 openBlock/createElementBlock 与运行时收集共同建立。编译器识别结构稳定的区域(没有 v-if/v-for),运行时把带 PatchFlag 的动态节点收集进 dynamicChildren。重渲染时只遍历这个数组,静态子树完全跳过。一棵 1000 个节点的子树,如果只有 3 个动态节点,diff 只比较 3 个。[3]
静态提升:不依赖响应式数据的 VNode 提升到模块级别,只创建一次。事件处理函数也被缓存(_cache[0] || (_cache[0] = ...)),避免每次渲染创建新的函数引用。

常见追问

题目来源于真实面经(字节/腾讯/阿里前端面试、牛客网、haizlin/fe-interview),按难度递进。
答题要点: parse 解析、transform 转换、generate 生成。高分回答: “模板编译分三个阶段:
  1. parse:把模板字符串 <div>{{ msg }}</div> 解析成 AST。每个节点记录标签名、属性、子节点、指令等信息。
  2. transform:遍历 AST,给节点打标记。PatchFlag 标记动态绑定类型,静态节点标记为可提升,v-if/v-for 创建子 Block。
  3. generate:把优化后的 AST 转换成 JavaScript 代码字符串——就是 createElementVNode(...) 这种代码。
编译器的入口在 packages/compiler-core/src/compile.ts 的 baseCompile 函数中。”
答题要点: 用按位或组合标记,用按位与检查标记。高分回答: “PatchFlag 是二进制位掩码。编译器用按位或组合多个标记:PatchFlag.CLASS | PatchFlag.STYLE = 2 | 4 = 6。运行时用按位与检查:6 & PatchFlag.CLASS = 2(truthy),6 & PatchFlag.TEXT = 0(falsy)。位运算的开销极低——一条 CPU 指令就能完成。React 需要在运行时比较新旧 props 对象,而 PatchFlag 让 Vue 3 用一次位与判断就能确定要更新哪类属性。”
答题要点: Block Tree 是虚拟 DOM 的优化层,不改变虚拟 DOM 的本质。高分回答: “Block Tree 不是替代虚拟 DOM,而是在虚拟 DOM 之上的优化层。虚拟 DOM 仍然是 Vue 3 的核心——渲染函数返回 VNode 树,diff 比较新旧 VNode 树。Block Tree 的作用是减少 diff 的工作量:通过 dynamicChildren 数组,把’遍历整棵树’变成’只遍历动态节点’。这和 React 的虚拟 DOM 没有本质区别——都是用 JS 对象描述 DOM 结构。区别在于 Vue 3 的编译器在构建时就分析了模板的结构信息,React 的 JSX 在运行时才被处理。”
答题要点: 静态提升是 VNode 级别优化,React Compiler 的 memo 是组件级别优化。高分回答: “Vue 3 的静态提升是 VNode 级别——把不依赖响应式的 VNode 对象提升到模块级别,只创建一次。这意味着连 VNode 对象的内存分配都省了。React Compiler 的 memo 是组件/表达式级别——缓存组件的渲染结果或计算结果,减少不必要的 re-render。它也会缓存部分 JSX 元素,但不依赖模板 DSL 的结构分析,粒度与 Vue 的 VNode 静态提升不同。本质区别在于信息来源:Vue 的优化基于模板编译期的静态分析,React 的优化基于运行时的依赖追踪与编译器推断。”
答题要点: v-if 编译为三元表达式,v-for 编译为 renderList 调用。高分回答: “v-if 编译为三元表达式:v-if='show' 变成 show ? createVNode(...) : createCommentVNode(...)。每个分支是独立的 Block。v-for 编译为 renderList 函数调用:v-for='item in list' 变成 renderList(list, item => createVNode(...))。每一项创建独立的 VNode,用 key 标识。两者都会创建子 Block,打断父 Block 的 dynamicChildren 收集。所以深层嵌套的 v-if/v-for 会削弱 Block Tree 的优化效果。”
答题要点: API 为具名导出,打包工具可以静态分析。高分回答: “Vue 3 的 API 为具名导出:import { ref, computed } from 'vue'。打包工具(Vite/webpack)通过静态分析确定哪些 API 被使用了,未使用的会被 Tree-shaking 移除。Vue 2 做不到——全局 API 挂在 Vue 构造函数上,import Vue from 'vue' 会导入整个模块。Vue 3 在设计时就考虑了这一点——每个 API 都是独立模块,尽量减少交叉引用。但注意:Vue 3 的内部模块之间有依赖关系,真正的 Tree-shaking 效果取决于依赖图的复杂度。尤雨溪在设计时就确保了核心模块(reactivity、runtime-core、runtime-dom)之间的依赖是单向的。”
答题要点: 静态节点直接输出 HTML 字符串,连 VNode 都不需要创建。高分回答: “编译器的静态分析在 SSR 中价值更大。SSR 的核心目标是把组件渲染成 HTML 字符串,如果每个节点都先创建 VNode 再序列化为 HTML,开销很大。Vue 3 的编译器为 SSR 生成专门的代码路径:完全静态的子树直接输出预编译的 HTML 字符串(_hoisted_1 = '<div>...</div>'),连 VNode 都不需要创建。动态节点用模板字符串插值(\${_ctx.msg}),直接拼接 HTML。这意味着一个有 1000 个静态节点 + 3 个动态节点的页面,SSR 只需要处理 3 个动态节点的序列化,静态部分直接输出字符串。相比 Vue 2,Vue 3 在 SSR 上做了更完整的静态优化,这是性能提升的重要原因之一。”
答题要点: Vue 编译输出渲染函数 + 虚拟 DOM,Svelte 编译输出直接操作 DOM 的命令式代码。高分回答: “Vue 3 的编译器输出的是渲染函数——运行时仍然需要虚拟 DOM 和 diff。Svelte 的编译器输出的是直接操作 DOM 的命令式代码——没有虚拟 DOM,变量变化时直接更新对应的 DOM 节点。Svelte 的优势:没有虚拟 DOM 的额外开销(创建 VNode、diff、遍历),编译后的代码更小。Svelte 的劣势:编译时分析有局限性(动态组件、条件渲染等复杂场景处理较难),没有跨平台能力。Vue 3 的虚拟 DOM 也有优势:跨平台(SSR、Weex、Canvas)、更好的 TypeScript 类型推断、Block Tree 优化后接近 Svelte 的性能。尤雨溪曾表示 Vue 3 的 Vapor Mode(无 VDOM 模式)正在探索中,未来可能结合两者的优势。“

延伸阅读与引用参考