Skip to content

第 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 的编译器已经默默做了三件事:

  1. PatchFlags —— 在编译期标记每个动态节点的变化类型,让运行时 Diff 只比较真正会变的部分
  2. Block Tree —— 将动态节点"拍平"到一个数组中,跳过静态子树的逐层遍历
  3. 静态提升 —— 将永远不会变化的 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-domcompile()
产物预编译的 .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增强后的 ASTJavaScript 代码字符串遍历 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: '静态文本'
            }
          ]
        }
      ]
    }
  ]
}

Parse 阶段的关键特征:

  1. 无优化:Parse 阶段不做任何优化判断,它只忠实地将模板结构转换为树形数据。"这个节点是否是静态的"不是 Parse 的职责。
  2. 容错处理:解析器能处理不规范的 HTML(如未闭合标签),并给出有意义的错误提示。
  3. 位置信息:每个 AST 节点都携带 loc(location)信息,精确到行号和列号,用于后续的 source map 生成和错误报告。

阶段二:Transform —— 从"是什么"到"怎么做"

Transform 是编译器中最复杂、最核心的阶段。如果说 Parse 回答的是"模板的结构是什么",那么 Transform 回答的是"这个结构应该如何被渲染"。

Transform 阶段的工作包括:

  1. 指令处理:将 v-ifv-forv-onv-bindv-model 等指令转换为对应的运行时代码结构
  2. 静态分析:判断哪些节点是完全静态的、哪些属性会动态变化
  3. PatchFlags 标记:为每个动态节点计算精确的变化标记
  4. Block 收集:将动态节点收集到 Block 的 dynamicChildren 数组
  5. 静态提升标记:标记可以被提升到渲染函数外部的静态节点
  6. codegenNode 生成:为每个节点创建 codegenNode,这是 Codegen 阶段的直接输入

Transform 采用插件架构。核心引擎提供遍历和上下文管理,具体的转换逻辑由一组"转换函数"实现:

typescript
// compiler-core/src/transform.ts(简化)
function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)

  // 静态提升(在遍历完成后执行)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }

  // 创建根节点的 codegenNode
  createRootCodegen(root, context)
}

// 遍历引擎
function traverseNode(node: TemplateNode, context: TransformContext) {
  context.currentNode = node

  // 执行所有转换插件(进入阶段)
  const { nodeTransforms } = context
  const exitFns: Function[] = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) exitFns.push(onExit)  // 收集退出回调
  }

  // 递归遍历子节点
  traverseChildren(node, context)

  // 执行退出回调(逆序 —— 后进先出)
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

🔥 深度洞察

Transform 的插件执行采用了"洋葱模型":每个插件在进入节点时可以返回一个"退出回调"。子节点的转换在进入和退出之间执行。这意味着退出回调执行时,所有子节点都已经被转换完毕。这个设计至关重要——像 v-if 这样的结构性指令需要在子节点转换完成后才能生成最终的 codegenNode,因为它需要知道子节点的动态特性。

核心转换插件列表:

插件文件职责
transformElementtransformElement.ts处理普通元素,生成 createVNode 调用
transformTexttransformText.ts合并相邻文本和插值节点
transformSlotOutlettransformSlotOutlet.ts处理 <slot> 出口
transformOncevOnce.ts处理 v-once,标记为缓存节点
transformIfvIf.ts处理 v-if/v-else-if/v-else,生成条件分支
transformForvFor.ts处理 v-for,生成列表渲染
transformBindvBind.ts处理 v-bind: 缩写)
transformOnvOn.ts处理 v-on@ 缩写)
transformModelvModel.ts处理 v-model,生成双向绑定代码

阶段三:Codegen —— 从树到字符串

代码生成器遍历 Transform 阶段产出的增强 AST(具体来说是每个节点的 codegenNode),将其转换为 JavaScript 代码字符串。

对于前面的模板示例,Codegen 产出的渲染函数大致如下:

javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

// 静态提升的节点
const _hoisted_1 = { class: "container" }
const _hoisted_2 = /*#__PURE__*/ _createElementVNode("p", null, null, -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("span", {
      title: _ctx.tooltip
    }, "静态文本", 8 /* PROPS */, ["title"])
  ]))
}

基于 VitePress 构建