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: '静态文本'
            }
          ]
        }
      ]
    }
  ]
}

基于 VitePress 构建