Appearance
第 7 章 Vue Compiler 架构总览
本章要点
- 编译器在 Vue 运行时体系中的定位:为什么"模板→渲染函数"是性能的关键战场
- 三阶段流水线:Parse → Transform → Codegen 的职责边界与数据流
- AST 节点类型体系:从 RootNode 到 SimpleExpressionNode 的完整族谱
- PatchFlags:编译期的"体检报告",运行时 Diff 的加速密钥
- Block Tree:打破虚拟 DOM 逐层比对的结构性飞跃
- 静态提升(Static Hoisting):让不变的节点只创建一次
- 编译器与响应式系统、渲染器的三角协作模型
2016 年,Evan You 在 Vue 2 中做了一个大胆的决定:模板编译不是可选的预处理步骤,而是框架的一等公民。彼时 React 阵营正在推崇 JSX 的"JavaScript 即模板"哲学,Angular 则将模板编译深藏在 CLI 工具链的黑盒中。Vue 选择了第三条路——模板和 JSX 都支持,但模板是默认的、推荐的、也是可以被深度优化的。
这个决定的价值在 Vue 3 中彻底兑现。当 React 还在为"是否需要编译器"争论(直到 React Compiler/React Forget 才姗姗来迟),Vue 3 的编译器已经默默做了三件事:
- PatchFlags —— 在编译期标记每个动态节点的变化类型,让运行时 Diff 只比较真正会变的部分
- Block Tree —— 将动态节点"拍平"到一个数组中,跳过静态子树的逐层遍历
- 静态提升 —— 将永远不会变化的 VNode 提升到渲染函数之外,避免每次渲染重复创建
这三项优化的共同特点是:它们只能在编译期完成。没有编译器,运行时就是"盲人摸象"——它不知道哪些节点是静态的,哪些属性会变,哪些子树可以跳过。有了编译器,运行时变成了"精确制导"——每一次比对、每一次更新都直奔目标。
本章将从宏观视角审视 Vue Compiler 的完整架构。我们不会深入每一行源码(那是第 8 章的任务),而是要建立一个清晰的心智模型:编译器由哪些阶段组成?每个阶段的输入和输出是什么?PatchFlags、Block Tree、静态提升分别在哪个阶段被计算?它们如何协同工作,让 Vue 的渲染性能远超纯运行时方案?
7.1 编译器在 Vue 架构中的位置
从模板到像素:完整渲染链路
一个 .vue 文件从编写到最终渲染在屏幕上,经历了这样一条链路:
编译器的职责很明确:将模板字符串转换为渲染函数。这个渲染函数在每次组件更新时被调用,返回新的 VNode 树,然后由渲染器(Renderer)对比新旧 VNode 树,将差异应用到真实 DOM。
编译时机:AOT vs JIT
Vue 编译器有两种运行时机:
| 维度 | AOT(预编译) | JIT(运行时编译) |
|---|---|---|
| 时机 | 构建阶段(Vite/Webpack) | 浏览器运行时 |
| 入口 | @vue/compiler-sfc | @vue/compiler-dom 的 compile() |
| 产物 | 预编译的 .js 文件 | 内存中的渲染函数 |
| 体积 | 不需要运行时编译器(~14KB 更小) | 需要完整构建版本 |
| 优化 | 可以执行所有静态分析优化 | 同样支持全部优化 |
| 使用场景 | 生产环境(推荐) | 动态模板、CDN 引入、在线编辑器 |
🔥 深度洞察
很多开发者认为"运行时编译 = 没有优化",这是一个误解。Vue 3 的运行时编译器和预编译器共享同一套优化流水线——PatchFlags、Block Tree、静态提升在两种模式下都会被应用。区别仅在于编译发生的时间点和产物形态。不过,AOT 编译允许 SFC 专属的优化(如
<script setup>的变量分析、CSS 变量注入),这些是运行时编译器无法做到的。
编译器的包结构
Vue 3 的编译器代码分布在三个包中:
packages/
├── compiler-core/ # 平台无关的编译核心
│ ├── parse.ts # 模板解析器 → AST
│ ├── transform.ts # AST 转换引擎
│ ├── codegen.ts # 代码生成器
│ └── transforms/ # 内置转换插件
├── compiler-dom/ # DOM 平台专属编译
│ ├── index.ts # compile() 入口
│ └── transforms/ # DOM 专属转换(v-html, v-model 等)
└── compiler-sfc/ # 单文件组件编译
├── parse.ts # SFC 解析(<template>/<script>/<style>)
├── compileTemplate.ts
├── compileScript.ts
└── compileStyle.ts这种分层设计体现了 Vue 3 的"平台抽象"哲学:compiler-core 不知道 DOM 的存在,它只处理纯粹的模板语法到 AST 到代码字符串的转换。DOM 特定的规则(哪些是原生元素、哪些属性需要特殊处理)由 compiler-dom 以插件形式注入。这意味着同一个编译核心可以被复用来编译 SSR 代码、原生渲染代码,甚至未来的 Vapor Mode 代码。
7.2 三阶段流水线:Parse → Transform → Codegen
全景数据流
Vue 编译器的核心是一个经典的三阶段流水线:
每个阶段有明确的输入和输出:
| 阶段 | 输入 | 输出 | 核心职责 |
|---|---|---|---|
| Parse | 模板字符串 <div>{{msg}}</div> | 模板 AST(树状节点结构) | 词法分析 + 语法分析,识别元素、属性、指令、插值 |
| Transform | 模板 AST | 带有 codegenNode 的增强 AST | 语义分析、优化标记(PatchFlags)、静态提升、Block 收集 |
| Codegen | 增强后的 AST | JavaScript 代码字符串 | 遍历 AST 生成 render() 函数的源码 |
阶段一:Parse —— 从字符串到树
解析器的任务是将模板字符串转换为抽象语法树(AST)。Vue 的模板解析器是一个手写的递归下降解析器,不依赖任何第三方库。
一个简单的模板:
html
<div class="container">
<p>{{ message }}</p>
<span :title="tooltip">静态文本</span>
</div>被解析为这样的 AST:
typescript
// 简化的 AST 结构
const ast: RootNode = {
type: NodeTypes.ROOT,
children: [
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'class',
value: { content: 'container' }
}
],
children: [
{
type: NodeTypes.ELEMENT,
tag: 'p',
children: [
{
type: NodeTypes.INTERPOLATION, // 插值
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'message',
isStatic: false
}
}
]
},
{
type: NodeTypes.ELEMENT,
tag: 'span',
props: [
{
type: NodeTypes.DIRECTIVE, // v-bind
name: 'bind',
arg: { content: 'title', isStatic: true },
exp: { content: 'tooltip', isStatic: false }
}
],
children: [
{
type: NodeTypes.TEXT,
content: '静态文本'
}
]
}
]
}
]
}