Vue 模板编译原理
Vue 模板如何变成可执行的 render function?
Vue 模板编译的核心流程可以概括为:
1. 为什么需要模板编译?
浏览器不能直接理解 Vue 的模板语法:
Vue 需要把模板转换成 JavaScript render function。组件运行时执行 render function,生成 VNode,再由 renderer 将 VNode patch 到真实 DOM。
2. Parse:模板解析为 AST
Parse 阶段会读取模板字符串,识别:
- 元素节点;
- 文本和插值节点;
- 属性和指令;
- 注释;
- 条件、循环等结构。
例如:
会被解析成类似这样的 AST:
AST 是后续分析和生成代码的中间表示。
3. Transform:分析和优化 AST
Transform 阶段会遍历 AST,将模板语义转换成运行时需要的信息,例如:
- 普通元素转换为 VNode 创建调用;
v-if转换为条件表达式;v-for转换为列表渲染逻辑;- 插值转换为读取响应式状态的表达式;
- 事件和属性转换为 props;
- 标记静态节点、动态属性和动态子树。
Vue 3 会在这里生成 Patch Flag、Block 等优化信息,让运行时只关注真正可能变化的部分。
4. Codegen:生成 render function
最终会生成近似下面的逻辑:
实际 Vue 3 编译产物会使用 openBlock、createElementBlock 等内部函数,并附带动态节点标记。这里的代码只是帮助理解的简化版本。
5. 编译时优化
静态提升
不会变化的 VNode 或 props 可以提升到 render function 外部,避免每次渲染重复创建。
Patch Flag
Patch Flag 标记的是单个 VNode 内部哪些内容可能变化,例如只有文本变化、只有 class 变化,或只有部分 props 变化。
这个 div 本身是动态节点,编译器可以给它加上类似 TEXT | CLASS 的标记。运行时 patch 到这个节点时,就不需要完整比较所有属性和子节点,只需要更新文本和 class。
Block Tree
Block Tree 解决的是更新时如何快速找到动态节点。如果每次组件更新都从根节点递归遍历整棵 VNode 树,即使动态节点带了 Patch Flag,也仍然要穿过大量静态层级。
这里真正会变的只有 span。Vue 3 会让当前 Block 收集动态后代,形成类似 dynamicChildren = [span] 的结构。组件更新时可以直接遍历这组动态节点,跳过 header、main、footer 这些静态层级。
所以两者的区别是:
- Block Tree:决定“这次更新需要看哪些节点”,避免遍历整棵树;
- Patch Flag:决定“看到了某个动态节点后,需要更新它的哪些部分”,避免完整比较节点内容。
它们通常配合使用:Block Tree 先把运行时带到动态节点面前,Patch Flag 再告诉运行时这个节点应该怎么精确更新。
事件处理函数缓存
部分静态事件处理器可以被缓存,减少每次 render 创建新函数的成本。
6. 运行时编译与构建时编译
- 运行时编译:浏览器中携带模板编译器,运行时把 template 编译为 render function,包体更大。
- 构建时编译:在构建阶段完成编译,线上只保留运行时,通常体积更小、启动更快。
现代 Vue 项目通常使用 SFC 编译器在构建阶段处理 <template>。
7. 常见误区
- Vue 模板不是直接转换成 HTML 字符串,而是通常转换成 render function。
- 编译优化不能替代正确使用
key和合理的组件边界。 - Patch Flag 是运行时提示,不是跳过所有 Diff。
- 模板编译与响应式系统是两条链路:编译负责生成渲染代码,响应式负责触发重新渲染。
面试总结
Vue 模板会先经过 Parse 生成 AST,再经过 Transform 完成指令转换和静态分析,最后由 Codegen 生成 render function。运行时执行 render function 生成 VNode,renderer 再通过 patch 更新 DOM。Vue 3 还通过静态提升、Patch Flag 和 Block Tree 缩小运行时更新范围。

