你在 .vue 文件里写的 <template> 并不是直接跑在浏览器里的 HTML,它会被 Vue 的编译器转成一段纯 JavaScript 的渲染函数。这个过程分为三步:解析、优化、生成。运行时实际执行的,就是编译产出的渲染函数,模板本身并不参与运行。
解析阶段:把模板字符串变成 AST
编译器拿到模板字符串后,首先要读懂它。这个过程很像浏览器解析 HTML:逐字符读取,识别出开始标签、结束标签、属性、文本内容、插值表达式 {{ }} 以及各种指令。
<div id="app">
<span>Hello</span>
<p>{{ message }}</p>
</div>
这一步会生成一棵抽象语法树(AST),每个节点描述了它在模板中的类型(元素节点、文本节点、插值节点)、标签名、属性和父子关系。比如上面的模板会被结构化成一棵树,其中 <span>Hello</span> 是 div 的子节点,“Hello” 是一个纯文本节点,{{ message }} 是一个插值节点。
解析阶段还会处理一些模板特有的语法,比如 v-for、v-if、动态属性 :src、事件 @click,并把它们全部规范化到 AST 的对应字段中。最终产出的 AST 是后续两个阶段的基础,也保留了源码位置信息,方便开发工具提示和报错。
优化阶段:标记动静,为运行时减负
直接拿 AST 生成渲染函数也能跑,但 Vue 3 的编译器会额外做一次优化——分析哪些节点是“静态的”(永远不会变),哪些是“动态的”(可能随响应式数据变化),并在 AST 上打上标记。这是 Vue 3 渲染性能大幅提升的关键所在。
具体来说有两类核心优化:
1. 静态节点提升
如果一个节点及其所有子节点都不包含任何动态绑定(没有插值、指令、动态属性),它就是一个纯静态节点。优化阶段会把这种节点直接提取到渲染函数外部,并在 AST 上标记。这样在组件多次更新时,静态节点的 VNode 创建只会执行一次,后续 Diff 阶段直接复用,完全跳过对比。
2. PatchFlags 动态标记
对于动态节点,编译器会根据它变化的类型打上 PatchFlag。例如:
- 1:文本内容会变(
{{ msg }}) - 2:class 会变(
:class="cls") - 4:style 会变
- 8:props 会变
运行时 Diff 时,Vue 只看带 PatchFlag 的动态节点,并且只检查它们对应变化的属性,不需要全属性对比。
经过优化阶段,AST 上多了 hoisted、patchFlag 等标记信息,为生成阶段产出高效代码做准备。
生成阶段:把 AST 变成渲染函数代码
有了带优化标记的 AST,编译器最后一步就是递归遍历这棵树,生成一段可执行的 JavaScript 代码。这段代码就是组件的渲染函数。
例如,前面的模板经过生成阶段会变成类似这样的代码(简化示意):
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "Hello", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", { id: "app" }, [
_hoisted_1,
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
注意几点:
- 静态
<span>被提升到了_hoisted_1,渲染函数直接引用它,不需要每次重新创建。 - 动态
<p>的创建调用了_createElementVNode,并且末尾带了个1(PatchFlag TEXT),告诉运行时“只关心文本内容是否变化”。 _openBlock()和_createElementBlock()用于创建 Block 树,配合 PatchFlag 实现靶向更新。
在构建阶段(比如通过 Vite 的 @vitejs/plugin-vue),这些编译工作在打包时就已经完成,发送到浏览器里的只是最终的渲染函数代码,不包含任何编译器。所以用户下载的体积更小,运行时性能也更好。
整个编译过程就像翻译一本带插图的书:解析是把句子拆成单词和语法结构,优化是给那些永远不会变的段落加盖“静态章”,生成则是把整本书用目标语言重新写出来,并且处处考虑如何让读者读得更快。