编译管道全链路:parse → transform → codegen 的完整过程
第10章:编译管道全链路——parse → transform → codegen 的完整过程
Vue 3 的模板编译器不仅是语法转换工具——它是一个静态分析引擎。同样的模板,经过编译器的优化标注,运行时的 diff 计算量可以减少 90% 以上。
本章核心问题:<template> 里的字符串是怎样变成 createVNode 调用的?编译器在这个过程中做了哪些优化?为什么 Vue 3 的运行时可以如此高效?
读完本章你将理解:
- 编译器三个阶段的分工与数据流
- Parser 的状态机实现原理
- AST 节点类型系统
- Transform 的插件架构与各核心 transform 的职责
- codegen 如何从 AST 生成可执行的 render 函数字符串
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 */)
]))
}
注意:
_hoisted_1和_hoisted_2被提升到 render 函数外部(静态提升)_hoisted_2的 PatchFlag = -1(HOISTED),表示已提升,diff 时直接跳过<p>的 PatchFlag = 3(TEXT | CLASS),表示文本和 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 需要在 transformFor、transformIf 处理完子节点之后才能最终确定自身的 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(不允许 eval 和 new 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>' // 运行时编译失败!
})
解决方案:
- 始终使用构建时编译(.vue SFC + Vite/Webpack)
- 如果必须使用运行时编译,使用
@vue/vue3-jest的 CSP-friendly 版本 - 或者在 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>
本章小结
-
编译器是三阶段管道:Parser(字符串→AST)→ Transformer(AST→优化AST)→ Generator(优化AST→render函数字符串)。每个阶段专注于一件事,通过数据流串联。
-
Parser 是手写状态机,不是正则:状态机比正则更快(无回溯)、更精确(可以处理嵌套结构),也更容易给出准确的错误信息。Vue 3.4 对 Parser 做了完全重写,性能提升了 40-70%。
-
Transform 的插件架构让编译器可以扩展:
nodeTransforms和directiveTransforms构成了编译器的优化引擎,每个 transform 专注于一类优化(静态提升、PatchFlag 标注、v-if/v-for 处理等)。 -
双向遍历(enter/exit)解决了父子依赖问题:进入时收集优化信息,退出时(子节点已处理完毕)才生成最终的代码节点,这是 Transform 阶段设计最精妙之处。
-
codegen 是简单的 AST 遍历 + 字符串拼接:编译器的复杂性在 Parser 和 Transform,codegen 相对简单——它只是将结构化的 AST 节点按固定规则输出为 JavaScript 代码字符串,最终通过
new Function()变为可执行函数。