第 10 章

编译管道全链路:parse → transform → codegen 的完整过程

第10章:编译管道全链路——parse → transform → codegen 的完整过程

Vue 3 的模板编译器不仅是语法转换工具——它是一个静态分析引擎。同样的模板,经过编译器的优化标注,运行时的 diff 计算量可以减少 90% 以上。

本章核心问题<template> 里的字符串是怎样变成 createVNode 调用的?编译器在这个过程中做了哪些优化?为什么 Vue 3 的运行时可以如此高效?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

编译发生在哪里?

Vue 3 有两种使用方式,编译发生的时机不同:

构建时编译(推荐):使用 @vue/compiler-sfc 在构建阶段(Vite/Webpack)编译 .vue 文件中的 <template>。生产包里包含的是已编译的 render 函数,没有编译器代码,包体积更小。

运行时编译:使用 vue 的完整版(包含编译器),在浏览器里实时编译模板字符串。适用于不需要构建步骤的场景(CDN 引入),但包体积增加约 14KB(gzip 后)。

// 运行时编译(完整版 vue)
const app = Vue.createApp({
  template: '<div>{{ message }}</div>',  // 运行时编译
  data() { return { message: 'Hello' } }
})

// 构建时编译(推荐,SFC)
// <template> 在 .vue 文件里,由 vite-plugin-vue 调用编译器

三阶段概览

模板字符串
    │
    ▼ parse(解析)
AST(抽象语法树)
    │
    ▼ transform(转换/优化)
优化后的 AST + PatchFlag + 静态提升信息
    │
    ▼ generate(代码生成)
render 函数字符串
    │
    ▼ new Function(code)
可执行的 render 函数

用 vue-template-explorer 观察编译结果

在线工具 template.vuejs.org 允许你实时查看模板的编译结果。这是理解编译器行为最直观的方式。

示例1:简单插值

<!-- 模板 -->
<div class="title">{{ message }}</div>
// 编译结果
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { class: "title" }, _toDisplayString(_ctx.message), 1 /* TEXT */))
}

注意最后的 1 /* TEXT */——这是 PatchFlag,告诉运行时"这个节点只有文本内容是动态的"。

示例2:带动态绑定

<!-- 模板 -->
<div :class="cls" :style="sty">{{ msg }}</div>
// 编译结果
_createElementVNode("div", {
  class: _ctx.cls,
  style: _ctx.sty
}, _toDisplayString(_ctx.msg), 3 /* TEXT, CLASS */)

PatchFlag = 3 = 1(TEXT)| 2(CLASS),但没有 STYLE(4)——等一下,上面有 :style,为什么?

因为编译器检测到 :style 也是动态的,实际上 PatchFlag 会是 7 = 1+2+4

// 实际编译结果
_createElementVNode("div", {
  class: _ctx.cls,
  style: _ctx.sty
}, _toDisplayString(_ctx.msg), 7 /* TEXT, CLASS, STYLE */)

Level 2 · 它是怎么运行的(3-5年经验)

Phase 1:Parser——状态机解析器

Parser 是编译器的第一个阶段,将模板字符串转换为 AST。Vue 3 的 Parser 是一个手写的状态机(不使用正则表达式解析,因为状态机更高效且可控)。

Parser 的核心状态

State.DATA              // 初始状态,处理文本内容
State.RCDATA            // <textarea>/<title> 内的特殊文本
State.RAWTEXT           // <script>/<style> 内的原始文本
State.TAG_OPEN          // 遇到 '<',准备解析标签
State.END_TAG_OPEN      // 遇到 '</',准备解析结束标签
State.TAG_NAME          // 正在解析标签名
State.BEFORE_ATTRIBUTE  // 标签名结束,准备解析属性
State.ATTRIBUTE_NAME    // 正在解析属性名
State.ATTRIBUTE_VALUE   // 正在解析属性值
State.COMMENT           // <!-- 内的注释
State.INTERPOLATION     // {{ 内的插值

状态转换示例:解析 <div id="app">{{ msg }}</div>

初始: State.DATA
'<' → State.TAG_OPEN
'd' → State.TAG_NAME → 累积 "d"
'i' → 继续 → 累积 "di"
'v' → 继续 → 累积 "div"
' ' → State.BEFORE_ATTRIBUTE
'i' → State.ATTRIBUTE_NAME → 累积 "i"
'd' → 继续 → 累积 "id"
'=' → State.BEFORE_ATTRIBUTE_VALUE
'"' → State.ATTRIBUTE_VALUE (双引号)
'a' → 累积 "a"
'p' → 累积 "ap"
'p' → 累积 "app"
'"' → 属性结束 → 创建 attribute node {name:"id",value:"app"}
'>' → 标签结束 → 创建 Element node {tag:"div",props:[...]}
'{' → State.DATA,检测到 '{{'
'{' → State.INTERPOLATION
' ' → 忽略
'm' → 累积 "m"
's' → 累积 "ms"
'g' → 累积 "msg"
' ' → 忽略
'}' → 检测到 '}}'
'}' → 创建 Interpolation node {content: "msg"}
回到 State.DATA
'<' → State.TAG_OPEN
'/' → State.END_TAG_OPEN
'd','i','v' → 标签名 "div"
'>' → 匹配到开始标签 div,弹出栈

AST 节点类型完整表

Vue 3 编译器定义了丰富的 AST 节点类型:

// packages/compiler-core/src/ast.ts
export const enum NodeTypes {
  ELEMENT = 1,               // <div>...</div>
  ATTRIBUTE = 6,             // id="app"
  TEXT = 2,                  // 纯文本节点
  COMMENT = 3,               // <!-- 注释 -->
  SIMPLE_EXPRESSION = 4,     // 简单表达式(如 msg、count + 1)
  INTERPOLATION = 5,         // {{ msg }}
  COMPOUND_EXPRESSION = 8,   // 复合表达式("Hello " + name)
  IF = 9,                    // v-if 节点
  IF_BRANCH = 10,            // v-if/v-else-if/v-else 分支
  FOR = 11,                  // v-for 节点
  TEXT_CALL = 12,            // createTextVNode 调用
  VNODE_CALL = 13,           // createVNode/createElementVNode 调用
  JS_CALL_EXPRESSION = 14,   // 任意 JS 函数调用
  JS_OBJECT_EXPRESSION = 15, // JS 对象字面量
  JS_PROPERTY = 16,          // 对象的 key-value 对
  JS_ARRAY_EXPRESSION = 17,  // JS 数组字面量
  JS_FUNCTION_EXPRESSION = 18, // JS 函数表达式
  JS_CONDITIONAL_EXPRESSION = 19, // 三元表达式
  JS_CACHE_EXPRESSION = 20,  // 缓存表达式(事件处理器缓存)
  JS_BLOCK_STATEMENT = 21,   // JS 块语句
  JS_TEMPLATE_LITERAL = 22,  // 模板字面量
  JS_IF_STATEMENT = 23,      // if 语句
  JS_ASSIGNMENT_EXPRESSION = 24, // 赋值表达式
  JS_SEQUENCE_EXPRESSION = 25,   // 逗号表达式
  JS_RETURN_STATEMENT = 26,      // return 语句
}

一个元素节点的 AST 结构

// <div id="app" :class="cls">{{ msg }}</div> 解析后的 AST
{
  type: 1, // ELEMENT
  tag: 'div',
  tagType: 0, // ElementTypes.ELEMENT(原生元素)
  props: [
    {
      type: 6, // ATTRIBUTE(静态属性)
      name: 'id',
      value: { type: 2, content: 'app' } // TEXT
    },
    {
      type: 7, // DIRECTIVE(指令,包括 v-bind)
      name: 'bind',
      arg: { type: 4, content: 'class', isStatic: true }, // SIMPLE_EXPRESSION
      exp: { type: 4, content: 'cls', isStatic: false }
    }
  ],
  children: [
    {
      type: 5, // INTERPOLATION
      content: {
        type: 4, // SIMPLE_EXPRESSION
        content: 'msg',
        isStatic: false
      }
    }
  ],
  codegenNode: undefined // transform 后才会填充
}

Phase 2:Transform——AST 优化器

Transform 是编译器中工作量最大、设计最精妙的阶段。它遍历 AST,通过插件化的方式对节点进行分析和转换。

Transform 的插件架构

// 内置的 node transforms
const nodeTransforms = [
  transformOnce,      // v-once:标记为静态,只渲染一次
  transformIf,        // v-if/v-else-if/v-else
  transformMemo,      // v-memo
  transformFor,       // v-for
  ...(!__BROWSER__ ? [transformFilter] : []),  // 兼容性过滤器
  ...(!options.isBuiltInComponent ? [] : [transformSlotOutlet]),
  transformElement,   // 核心:处理元素节点
  trackSlotScopes,    // slot 作用域追踪
  transformText,      // 文本节点处理(合并相邻文本)
]

const directiveTransforms = {
  on: transformOn,           // v-on 指令
  bind: transformBind,       // v-bind 指令
  model: transformModel,     // v-model 指令
  show: transformShow,       // v-show 指令
  html: transformHtml,       // v-html 指令
  text: transformText,       // v-text 指令
}

Transform 的双向遍历(进入 + 退出)

遍历 AST 节点(深度优先)
    │
    ▼
进入(enter)节点
    │
    ├── 执行所有 nodeTransforms 的 enter 钩子
    │   (收集需要在退出时执行的回调)
    │
    ▼
递归处理子节点
    │
    ▼
退出(exit)节点
    │
    └── 执行进入时收集的退出回调(逆序)
        (此时子节点已处理完毕,可以使用子节点信息)

为什么需要退出钩子?因为某些转换(如 transformElement)需要先知道子节点的情况(有几个动态子节点?是否有 Block?)才能决定如何生成代码。

transformElement 的核心工作

// 简化示意
function transformElement(node, context) {
  if (node.type !== NodeTypes.ELEMENT) return
  
  // 返回退出函数(等子节点处理完后再执行)
  return () => {
    const { tag, props, children } = node
    
    // 1. 解析标签类型(原生/组件)
    const vnodeTag = isComponent ? resolveComponent(tag) : `"${tag}"`
    
    // 2. 处理 props,计算 patchFlag
    let patchFlag = 0
    let dynamicPropNames: string[] = []
    
    for (const prop of props) {
      if (prop.type === NodeTypes.DIRECTIVE) {
        if (prop.name === 'bind') {
          if (!prop.arg?.isStatic) {
            // 动态 key 绑定(:[\`${key}\`]="val")
            patchFlag |= PatchFlags.FULL_PROPS
          } else {
            dynamicPropNames.push(prop.arg.content)
            if (prop.arg.content === 'class') patchFlag |= PatchFlags.CLASS
            else if (prop.arg.content === 'style') patchFlag |= PatchFlags.STYLE
            else patchFlag |= PatchFlags.PROPS
          }
        }
      }
    }
    
    // 3. 处理 children,判断是否需要 key
    
    // 4. 生成 VNodeCall(codegenNode)
    node.codegenNode = createVNodeCall(
      context,
      vnodeTag,
      vnodeProps,
      vnodeChildren,
      patchFlag,
      dynamicPropNames,
    )
  }
}

Phase 3:codegen——代码生成器

codegen 接收优化后的 AST,遍历它并输出 JavaScript 字符串。

代码生成的核心方法

// 简化示意(packages/compiler-core/src/codegen.ts)

function genNode(node, context) {
  switch (node.type) {
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      genNode(node.codegenNode, context)  // 递归到 codegenNode
      break
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context)
      break
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break
    case NodeTypes.TEXT:
      genText(node, context)
      break
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.JS_CALL_EXPRESSION:
      genCallExpression(node, context)
      break
    // ... 等
  }
}

function genVNodeCall(node, context) {
  const { push, helper } = context
  const { tag, props, children, patchFlag, dynamicProps, isBlock } = node
  
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(), `)
  }
  
  const callHelper = isBlock ? CREATE_ELEMENT_BLOCK : CREATE_ELEMENT_VNODE
  push(helper(callHelper) + `(`)
  
  genNodeList([tag, props, children, patchFlag, dynamicProps], context)
  
  push(`)`)
  
  if (isBlock) {
    push(`)`)
  }
}

完整的编译输出示例

输入模板:

<template>
  <div class="container">
    <h1>标题</h1>
    <p :class="dynamicClass">{{ message }}</p>
  </div>
</template>

编译输出:

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

// 静态提升:纯静态的 VNode 只创建一次
const _hoisted_1 = { class: "container" }
const _hoisted_2 = _createElementVNode("h1", null, "标题", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", {
      class: _normalizeClass(_ctx.dynamicClass)
    }, _toDisplayString(_ctx.message), 3 /* TEXT, CLASS */)
  ]))
}

注意:


Level 3 · 设计文档与源码(资深开发者)

编译器的模块边界

Vue 3 的编译器被设计为多个独立包,支持跨平台编译:

packages/
├── compiler-core/          # 核心编译器(与平台无关)
│   ├── src/parse.ts        # Parser(状态机)
│   ├── src/transform.ts    # Transform(遍历器 + 插件系统)
│   ├── src/codegen.ts      # Generator(代码生成)
│   ├── src/ast.ts          # AST 节点类型定义
│   └── src/transforms/     # 内置 transform 插件
│       ├── transformElement.ts
│       ├── transformFor.ts
│       ├── transformIf.ts
│       └── ...
│
├── compiler-dom/           # DOM 平台特定编译器
│   ├── src/index.ts        # 组合 core + DOM transforms
│   └── src/transforms/
│       ├── transformModel.ts  # v-model(处理 DOM 特性)
│       └── ...
│
├── compiler-sfc/           # .vue 单文件组件编译器
│   └── src/               # 处理 <template>/<script>/<style>
│
└── compiler-ssr/           # SSR 服务端渲染编译器

parse 函数的核心循环

文件路径packages/compiler-core/src/parser.ts(Vue 3.4 重写版)

// Vue 3.4 使用了全新的、更快的 parser 实现
function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[],
): TemplateChildNode[] {
  const nodes: TemplateChildNode[] = []
  
  while (!isEnd(context, mode, ancestors)) {
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined
    
    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // 插值:{{ ... }}
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        if (s[1] === '!') {
          if (startsWith(s, '<!--')) {
            node = parseComment(context)
          } else if (startsWith(s, '<!DOCTYPE')) {
            node = parseBogusComment(context)
          }
        } else if (s[1] === '/') {
          // 结束标签
          if (/[a-z]/i.test(s[2])) {
            parseTag(context, TagType.End, parent)
            break  // 结束循环
          }
        } else if (/[a-z]/i.test(s[1])) {
          // 开始标签
          node = parseElement(context, ancestors)
        }
      }
    }
    
    if (!node) {
      node = parseText(context, mode)
    }
    
    if (Array.isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(nodes, node[i])
      }
    } else {
      pushNode(nodes, node)
    }
  }
  
  return nodes
}

transform 的遍历器实现

文件路径packages/compiler-core/src/transform.ts

export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext,
) {
  context.currentNode = node
  // 应用所有 nodeTransforms,收集 onExit 回调
  const { nodeTransforms } = context
  const exitFns: OnExit[] = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // 节点被删除(如 v-if 被条件移除)
      return
    } else {
      node = context.currentNode
    }
  }
  
  // 递归处理子节点
  switch (node.type) {
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
    case NodeTypes.INTERPOLATION:
    case NodeTypes.TEXT_CALL:
      traverseNode(node.content, context)
      break
  }
  
  // 逆序执行退出回调
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

逆序执行退出回调的意义:最后注册的 transform 最先退出,模拟了调用栈的先进后出顺序。这对有依赖关系的 transform 非常重要:transformElement 需要在 transformFortransformIf 处理完子节点之后才能最终确定自身的 VNodeCall。

hoistStatic:静态提升的判断逻辑

文件路径packages/compiler-core/src/transforms/hoistStatic.ts

function getStaticType(
  node: TemplateChildNode | SimpleExpressionNode,
  resultCache: Map<TemplateChildNode, StaticType> = new Map(),
): StaticType {
  switch (node.type) {
    case NodeTypes.ELEMENT:
      // 组件不提升(props 可能动态)
      if (node.tagType !== ElementTypes.ELEMENT) {
        return StaticType.NOT_STATIC
      }
      const cached = resultCache.get(node)
      if (cached !== undefined) {
        return cached
      }
      
      const codegenNode = node.codegenNode!
      if (codegenNode.type !== NodeTypes.VNODE_CALL) {
        return StaticType.NOT_STATIC
      }
      
      // 有 patchFlag 的节点不能提升(动态节点)
      if (
        codegenNode.patchFlag !== undefined &&
        codegenNode.patchFlag !== PatchFlags.NEED_PATCH
      ) {
        return StaticType.NOT_STATIC
      }
      
      // 有 v-once 的节点可以提升
      // 递归检查所有子节点
      let returnType = StaticType.FULL_STATIC
      for (const child of node.children) {
        const childType = getStaticType(child, resultCache)
        if (childType === StaticType.NOT_STATIC) {
          return StaticType.NOT_STATIC
        } else if (childType === StaticType.HAS_RUNTIME_CONSTANT) {
          returnType = StaticType.HAS_RUNTIME_CONSTANT
        }
      }
      
      resultCache.set(node, returnType)
      return returnType
      
    case NodeTypes.TEXT:
    case NodeTypes.COMMENT:
      return StaticType.FULL_STATIC  // 纯文本和注释总是静态的
      
    case NodeTypes.INTERPOLATION:
    case NodeTypes.COMPOUND_EXPRESSION:
      return StaticType.NOT_STATIC   // 插值总是动态的
      
    case NodeTypes.SIMPLE_EXPRESSION:
      return node.isStatic ? StaticType.FULL_STATIC : StaticType.NOT_STATIC
  }
}

Level 4 · 边界与陷阱(全体适用)

陷阱1:运行时编译的 CSP(内容安全策略)问题

new Function(code) 是 Vue 3 运行时编译的核心,但这在启用了严格 CSP(不允许 evalnew Function)的环境下会失败:

// 错误:CSP 阻止了 eval/new Function
// Refused to evaluate a string as JavaScript because 
// 'unsafe-eval' is not an allowed source of script in the 
// following Content Security Policy directive

const app = Vue.createApp({
  template: '<div>{{ msg }}</div>'  // 运行时编译失败!
})

解决方案

  1. 始终使用构建时编译(.vue SFC + Vite/Webpack)
  2. 如果必须使用运行时编译,使用 @vue/vue3-jest 的 CSP-friendly 版本
  3. 或者在 CSP 头中添加 'unsafe-eval'(降低安全性,不推荐)

陷阱2:模板中的表达式被编译为函数作用域内的代码

<!-- 模板里的表达式 -->
<div>{{ someGlobalVar }}</div>

编译后:

function render(_ctx, _cache) {
  return createVNode("div", null, toDisplayString(_ctx.someGlobalVar))
}

_ctx 是组件实例的代理,只代理了组件的 data/props/computed/methods 等。全局 JavaScript 变量(如 window.someVar)在模板中无法直接访问,会被当作 _ctx.someVar 处理,结果是 undefined

解决方案:通过 app.config.globalProperties 注册全局属性:

app.config.globalProperties.$myGlobal = someValue
// 模板里用 $myGlobal

陷阱3:v-for 中使用复杂表达式导致编译错误

<!-- ❌ 某些复杂解构可能在模板中无法正确编译 -->
<div v-for="{ a: { b }, c = 1 } in list">
  {{ b }} {{ c }}
</div>

Vue 3 的模板表达式解析有限制,复杂的 JavaScript 解构模式可能导致意外的编译结果或错误。

建议:将复杂逻辑移到 computed 或 script 层:

const processedList = computed(() => 
  list.value.map(({ a: { b }, c = 1 }) => ({ b, c }))
)
<div v-for="item in processedList">
  {{ item.b }} {{ item.c }}
</div>

陷阱4:模板中的 TypeScript 类型在运行时不存在

<!-- ❌ 类型断言在模板中不被支持 -->
<div>{{ (message as string).toUpperCase() }}</div>

模板编译器不支持 TypeScript 类型语法(如 as!、类型泛型)。这些语法会导致编译错误。

解决方案:在 <script setup> 中处理:

// 在 script 中
const upperMessage = computed(() => (message.value as string).toUpperCase())
<!-- 在模板中使用处理后的值 -->
<div>{{ upperMessage }}</div>

本章小结

  1. 编译器是三阶段管道:Parser(字符串→AST)→ Transformer(AST→优化AST)→ Generator(优化AST→render函数字符串)。每个阶段专注于一件事,通过数据流串联。

  2. Parser 是手写状态机,不是正则:状态机比正则更快(无回溯)、更精确(可以处理嵌套结构),也更容易给出准确的错误信息。Vue 3.4 对 Parser 做了完全重写,性能提升了 40-70%。

  3. Transform 的插件架构让编译器可以扩展nodeTransformsdirectiveTransforms 构成了编译器的优化引擎,每个 transform 专注于一类优化(静态提升、PatchFlag 标注、v-if/v-for 处理等)。

  4. 双向遍历(enter/exit)解决了父子依赖问题:进入时收集优化信息,退出时(子节点已处理完毕)才生成最终的代码节点,这是 Transform 阶段设计最精妙之处。

  5. codegen 是简单的 AST 遍历 + 字符串拼接:编译器的复杂性在 Parser 和 Transform,codegen 相对简单——它只是将结构化的 AST 节点按固定规则输出为 JavaScript 代码字符串,最终通过 new Function() 变为可执行函数。

本章评分
4.5  / 5  (35 评分)

💬 留言讨论