Vue Compiler 原理:从 template 到 render function
引言
Vue 的编译器是整个框架最精密的部分之一。它把声明式的 <template> 转换为可执行的渲染函数,并在这个过程中应用大量优化(静态提升、PatchFlag、Block Tree 等),让 Vue 3 在性能上具备和 React Fabric、Svelte 一较高下的资本。
理解 Vue 编译器原理,对以下场景都有帮助:
- 阅读编译产物,写出更高效的模板
- 自定义编译器插件,做 AST 转换、自定义指令
- 实现自定义 DSL(如 JSX→Vue)
- 面试、技术分享
本文将深入剖析 Vue 3 编译器的三阶段流程,并手写一个简化版编译器。
一、Vue 编译器整体架构
Vue 编译器位于 @vue/compiler-core(平台无关核心)+ @vue/compiler-dom(DOM 平台特定)+ @vue/compiler-sfc(SFC 编译)三层。
@vue/compiler-sfc ← 单文件组件编译
└── @vue/compiler-dom ← DOM 平台特定(v-html、style、class 等)
└── @vue/compiler-core ← 平台无关核心(parse / transform / generate)
编译流程分三阶段:
- Parse:将 template 字符串解析成 AST(抽象语法树)
- Transform:对 AST 进行转换、优化、生成语义信息
- Generate:将 AST 转换成可执行的渲染函数代码字符串
Template string
↓ parse
Template AST (原始)
↓ transform
JavaScript AST (含语义信息)
↓ generate
Render function code (string)
↓ new Function
Compiled render fn
二、Parse:从字符串到 AST
2.1 词法分析与语法分析
Vue 的 parser 采用手写递归下降的方式,而不是用 yacc 这类生成器。原因是 Vue 模板语法相对简单,手写可以更好地控制错误信息和性能。
2.2 AST 节点类型
export interface Node {
type: NodeTypes
loc: SourceLocation // 源码位置信息
}
export const enum NodeTypes {
ROOT, // 根节点
ELEMENT, // 元素 <div>
TEXT, // 文本
COMMENT, // 注释
INTERPOLATION, // 插值 {{ }}
SIMPLE_EXPRESSION,
ATTRIBUTE, // 静态属性
DIRECTIVE, // 指令 v-if/v-for/v-model
}
2.3 简化版 parser 实现
我们来手写一个简化版 parser,只支持元素、文本、插值:
// === 简化版 AST 类型 ===
type ASTNode =
| { type: 'Element'; tag: string; props: ASTProp[]; children: ASTNode[]; loc: Loc }
| { type: 'Text'; content: string; loc: Loc }
| { type: 'Interpolation'; content: string; loc: Loc }
// === Tokenizer ===
function tokenize(input: string): Token[] {
const tokens: Token[] = []
let i = 0
while (i < input.length) {
const rest = input.slice(i)
if (rest.startsWith('</')) {
const match = rest.match(/^<\/([a-zA-Z][\w-]*)\s*>/)
tokens.push({ type: 'tag-close', value: match[1], loc: { start: i, end: i + match[0].length } })
i += match[0].length
continue
}
if (rest.startsWith('<')) {
const match = rest.match(/^<([a-zA-Z][\w-]*)/)
tokens.push({ type: 'tag-open', value: match[1], loc: { start: i, end: i + match[0].length } })
i += match[0].length
while (input[i] !== '>' && i < input.length) i++
i++
continue
}
if (rest.startsWith('{{')) {
tokens.push({ type: 'mustache-open', value: '{{', loc: { start: i, end: i + 2 } })
i += 2
const closeIdx = input.indexOf('}}', i)
tokens.push({ type: 'text', value: input.slice(i, closeIdx).trim(), loc: { start: i, end: closeIdx } })
tokens.push({ type: 'mustache-close', value: '}}', loc: { start: closeIdx, end: closeIdx + 2 } })
i = closeIdx + 2
continue
}
const nextTag = input.indexOf('<', i)
const nextMustache = input.indexOf('{{', i)
let end = input.length
if (nextTag !== -1) end = Math.min(end, nextTag)
if (nextMustache !== -1) end = Math.min(end, nextMustache)
tokens.push({ type: 'text', value: input.slice(i, end), loc: { start: i, end } })
i = end
}
return tokens
}
// === Parser (递归下降) ===
function parse(input: string): ASTNode {
const tokens = tokenize(input)
let pos = 0
function parseChildren(): ASTNode[] {
const children: ASTNode[] = []
while (pos < tokens.length) {
const t = tokens[pos]
if (t.type === 'tag-close') { pos++; break }
if (t.type === 'tag-open') children.push(parseElement())
else if (t.type === 'mustache-open') children.push(parseInterpolation())
else if (t.type === 'text') {
children.push({ type: 'Text', content: t.value, loc: t.loc }); pos++
}
}
return children
}
function parseElement(): ASTNode {
const tag = tokens[pos].value as string
const startLoc = tokens[pos].loc.start
pos++
const children = parseChildren()
if (pos < tokens.length && tokens[pos].type === 'tag-close') pos++
return { type: 'Element', tag, props: [], children, loc: { start: startLoc, end: input.length } }
}
function parseInterpolation(): ASTNode {
pos++
const text = tokens[pos]; pos++; pos++
return { type: 'Interpolation', content: text.value, loc: text.loc }
}
return { type: 'Element', tag: 'root', props: [], children: parseChildren(), loc: { start: 0, end: input.length } }
}
三、Transform:AST 转换与优化
3.1 转换插件系统
Vue 编译器的 transform 阶段采用插件化架构,每个转换逻辑都是一个 transform plugin:
const nodeTransforms = [
trackVForSlotScopes,
transformExpression, // 表达式重写(_ctx.x)
transformIf, // v-if / v-else-if / v-else
transformOnce, // v-once
transformRef, // ref
transformModel, // v-model
transformOn, // v-on
transformBind, // v-bind
transformElement, // 元素 → createVNode 调用
transformText, // 文本合并
]
3.2 静态提升(Static Hoisting)
Vue 编译器会把完全静态的节点提升到 render 函数外部:
<template>
<div>
<p class="static">静态文本</p>
<p>{{ dynamic }}</p>
</div>
</template>
// 编译产物
const _hoisted_1 = createElementVNode('p', { class: 'static' }, '静态文本')
function render(_ctx, _cache) {
return (openBlock(), createElementBlock('div', null, [
_hoisted_1,
createElementVNode('p', null, toDisplayString(_ctx.dynamic), 1 /* TEXT */),
]))
}
3.3 PatchFlag
PatchFlag 是编译阶段为每个动态节点打上的标记,告诉运行时 diff 算法"这个节点哪些部分变了":
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态非 class/style 属性
FULL_PROPS = 1 << 4, // 有动态 key,需要全量 diff
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
HOISTED = -1, // 静态提升的节点
BAIL = -2, // 跳过优化模式
}
3.4 Block Tree
Vue 3 引入 Block Tree:把动态节点"扁平化"收集到根节点的 dynamicChildren 数组,diff 时只遍历这个数组,跳过所有静态节点。
function render(_ctx, _cache) {
return (openBlock(), createElementBlock('div', null, [
createVNode('header', null, '静态 header', -1 /* HOISTED */),
createVNode('main', null, [
createVNode('p', null, toDisplayString(_ctx.dynamic1), 1 /* TEXT */),
createVNode('p', { class: 'static' }, '静态 p', -1),
createVNode('p', null, toDisplayString(_ctx.dynamic2), 1),
]),
createVNode('footer', null, '静态 footer', -1),
]))
}
运行时 patch 时,只需要遍历 2 个动态 <p> 节点就完成 diff,而不是遍历整棵树。
3.5 缓存事件 handler
function render(_ctx, _cache) {
return createElementVNode('button', {
onClick: _cache[0] || (_cache[0] = () => _ctx.count++)
}, toDisplayString(_ctx.count), 1 /* TEXT */)
}
四、Generate:从 AST 到代码
简化版 generate
function generate(ast: ASTNode): string {
const hoists: string[] = []
function genNode(node: ASTNode): string {
switch (node.type) {
case 'Text':
return JSON.stringify(node.content)
case 'Interpolation':
return `toDisplayString(_ctx.${node.content})`
case 'Element':
const hoisted = isStatic(node)
const code = `createElementVNode(${JSON.stringify(node.tag)}, null, [${
node.children.map(genNode).join(', ')
}]${hoisted ? ', -1 /* HOISTED */' : ''})`
if (hoisted) {
const idx = hoists.length
hoists.push(`const _hoisted_${idx + 1} = ${code}`)
return `_hoisted_${idx + 1}`
}
return code
}
}
const body = genNode(ast)
return `
import { createElementVNode, toDisplayString, openBlock, createElementBlock } from 'vue'
${hoists.join('\n')}
export function render(_ctx, _cache) {
return (openBlock(), createElementBlock('div', null, [${body}]))
}
`
}
五、编译优化总结
| 优化技术 | 作用 | 编译阶段 |
|---|---|---|
| 静态提升 | 静态节点只创建一次 | transform |
| PatchFlag | 标记动态部分,diff 跳过未变 | transform |
| Block Tree | 动态节点扁平化收集 | transform |
| 缓存事件 handler | 内联事件缓存 | transform |
| 模板预编译 | 构建 .vue 时已完成编译 | SFC loader |
| Fragment 优化 | List 渲染的 key 检查 | transform |
六、Vue vs React vs Svelte 编译策略对比
| 维度 | Vue (AOT) | React (JIT) | Svelte (AOT) |
|---|---|---|---|
| 编译时机 | 构建期 | 无(运行时 VDOM) | 构建期 |
| 运行时大小 | 中 | 中 | 最小 |
| 优化能力 | 中等 | 有限 | 极致 |
| 动态性 | 强 | 强 | 中等 |
Vue 的 AOT 方案在保留 VDOM 灵活性的同时,通过编译优化让性能接近 Svelte。
七、实战应用:自定义编译器插件
import { parse, transform, NodeTypes } from '@vue/compiler-core'
// 插件:自动埋点 v-track 指令
const transformTrack = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return
const track = node.props.find(p =>
p.type === NodeTypes.DIRECTIVE && p.name === 'track'
)
if (track) {
context.replaceNode({
type: NodeTypes.ELEMENT,
tag: node.tag,
props: [
...node.props.filter(p => p !== track),
createOnDirective('click', `$track('${track.exp.content}')`)
],
children: node.children,
} as any)
}
}
总结
Vue 编译器通过 parse → transform → generate 三阶段,把声明式模板转换为高性能渲染函数。其中 transform 阶段引入静态提升、PatchFlag、Block Tree 等优化,让 Vue 3 在灵活性和性能之间找到了优秀的平衡。
理解编译器原理不仅能让我们写出更高性能的模板,还能通过自定义 transform 扩展编译能力。
Comments | 0条评论