05-Vue 编译器原理
Vue 编译器原理
Vue 的编译器将模板字符串转换为高效的渲染函数,是连接声明式模板和虚拟 DOM 运行时的桥梁。理解编译器原理,才能真正掌握 Vue 的工作机制。
一、前言
当你写下一个 Vue 模板时,框架内部经历了一系列复杂的转换过程:
模板字符串 → 词法分析 → 语法分析 → AST → 转换 → 代码生成 → 渲染函数
Vue3 的编译器位于 compiler-core 包中,采用模块化架构,compiler-dom 在核心基础上增加了 DOM 特定的优化。本章将深入解析编译器的完整工作流程。
二、编译流程概述
2.1 三大阶段
Vue 编译器的工作分为三个阶段:
2.2 编译入口
// packages/compiler-core/src/compile.ts
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
// 1. 解析模板为 AST
const ast = parse(template, options)
// 2. 转换 AST(应用编译时优化)
transform(ast, {
...options,
nodeTransforms: [
// 内置转换插件
transformOnce,
transformIf,
transformFor,
transformExpression,
transformSlotOutlet,
transformElement,
trackSlotScopes,
trackVForSlotScopes,
transformText,
...(options.nodeTransforms || [])
],
directiveTransforms: {
// 指令转换
bind: transformBind,
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow,
...(options.directiveTransforms || {})
}
})
// 3. 生成代码
return generate(ast, options)
}
三、模板解析:Tokenizer 与 Parser
3.1 词法分析(Tokenizer)
词法分析器将模板字符串切分为一系列 Token:
// 解析器状态机
export const enum TextModes {
DATA, // 普通 HTML 文本
RCDATA, // 可包含实体的文本(textarea, title)
RAWTEXT, // 原始文本(style, script)
CDATA,
ATTRIBUTE_VALUE // 属性值
}
// Token 类型
interface Token {
type: TokenType
content: string
loc: SourceLocation
}
enum TokenType {
TagOpen, // <div
TagEnd, // </div>
Text, // 文本内容
Interpolation // {{ expr }}
}
3.2 语法分析(Parser)
语法分析器将 Token 流构建为 AST 节点树。
// AST 节点类型
export const enum NodeTypes {
ROOT, // 根节点
ELEMENT, // 元素节点
TEXT, // 文本节点
COMMENT, // 注释节点
SIMPLE_EXPRESSION, // 简单表达式
INTERPOLATION, // 插值表达式 {{ }}
ATTRIBUTE, // 属性
DIRECTIVE, // 指令 v-
COMPOUND_EXPRESSION, // 复合表达式
IF, // v-if
IF_BRANCH, // v-if 分支
FOR, // v-for
TEXT_CALL, // 文本调用节点
VNODE_CALL, // VNode 调用节点
JS_CALL_EXPRESSION, // JS 调用表达式
JS_OBJECT_EXPRESSION, // JS 对象表达式
JS_PROPERTY, // JS 属性
JS_ARRAY_EXPRESSION, // JS 数组表达式
JS_FUNCTION_EXPRESSION,// JS 函数表达式
JS_CONDITIONAL_EXPRESSION, // 三元表达式
JS_CACHE_EXPRESSION // 缓存表达式
}
3.3 解析过程示例
<template>
<div class="container" @click="handleClick">
<p v-if="show">{{ message }}</p>
</div>
</template>
解析后的 AST:
{
type: NodeTypes.ROOT,
children: [
{
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'class',
value: { content: 'container' }
},
{
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: { content: 'click' },
exp: { content: 'handleClick' }
}
],
children: [
{
type: NodeTypes.IF,
branches: [
{
type: NodeTypes.IF_BRANCH,
condition: { content: 'show' },
children: [
{
type: NodeTypes.ELEMENT,
tag: 'p',
children: [
{
type: NodeTypes.INTERPOLATION,
content: { content: 'message' }
}
]
}
]
}
]
}
]
}
]
}
3.4 解析器核心逻辑
// packages/compiler-core/src/parser.ts
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
const nodes: TemplateChildNode[] = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node: TemplateChildNode | undefined
if (startsWith(s, '{{')) {
// 解析插值表达式
node = parseInterpolation(context, mode)
} else if (s[0] === '<') {
if (/[a-z]/i.test(s[1])) {
// 解析元素标签
node = parseElement(context, ancestors)
} else if (s[1] === '/') {
// 结束标签
parseEndTag(context)
continue
}
}
if (!node) {
// 解析文本节点
node = parseText(context, mode)
}
nodes.push(node)
}
return nodes
}
四、AST 转换(Transform)
4.1 转换器架构
Transform 阶段通过访问者模式遍历 AST,应用各种转换插件:
4.2 v-if 转换
// packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// 创建 codegenNode
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
0,
context
)
} else {
// 处理 else / else-if
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
ifNode.branches.length - 1,
context
)
}
}
})
}
)
// 转换结果:条件表达式
// v-if="show" → show ? _createElementVNode(...) : _createCommentVNode(...)
4.3 v-for 转换
// packages/compiler-core/src/transforms/vFor.ts
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
const { helper } = context
return processFor(node, dir, context, () => {
// 创建 renderList 调用
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source, // 遍历的数据源
createFunctionExpression(
forNode.parseResult,
forNode.children,
false,
true
)
])
return renderExp
})
}
)
// 转换结果:
// v-for="item in list" → _renderList(list, (item) => _createElementVNode(...))
4.4 静态提升(Static Hoisting)
// packages/compiler-core/src/transforms/hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(root, context, false)
}
function walk(node: ParentNode, context: TransformContext, isInVOnce: boolean) {
let hasHoistedNode = false
const children = node.children
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 判断是否为纯静态节点
if (child.type === NodeTypes.ELEMENT && isStaticNode(child, context)) {
// 转换为常量表达式
child.codegenNode = context.hoist(child.codegenNode!)
hasHoistedNode = true
continue
}
// 递归处理子节点
if (child.type === NodeTypes.ELEMENT) {
walk(child, context, isInVOnce)
}
}
}
// 判断节点是否纯静态
function isStaticNode(node: TemplateChildNode, context: TransformContext): boolean {
switch (node.type) {
case NodeTypes.ELEMENT:
// 没有动态绑定、没有指令、没有插槽
if (node.tagType !== ElementTypes.ELEMENT) return false
if (node.props.length > 0) {
return node.props.every(prop =>
prop.type === NodeTypes.ATTRIBUTE && !prop.value
)
}
return node.children.every(child => isStaticNode(child, context))
case NodeTypes.TEXT:
return true
case NodeTypes.INTERPOLATION:
case NodeTypes.COMPOUND_EXPRESSION:
return false
default:
return false
}
}
五、代码生成(Codegen)
5.1 生成器架构
// packages/compiler-core/src/codegen.ts
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const { mode, push, prefixIdentifiers, indent, deindent, newline } = context
// 生成前置代码(imports, const declarations)
genFunctionPreamble(ast, context)
// 生成 render 函数
const functionName = 'render'
const args = ['_ctx', '_cache']
const signature = args.join(', ')
push(`function ${functionName}(${signature}) {`)
indent()
// 生成函数体
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push('null')
}
deindent()
push('}')
return {
ast,
code: context.code,
map: options.sourceMap ? context.map : undefined
}
}
5.2 节点代码生成
// 根据节点类型分发到不同的生成器
function genNode(node: CodegenNode | symbol, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
switch (node.type) {
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
}
}
5.3 VNode 调用生成
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context
const { tag, props, children, patchFlag, dynamicProps, directives, isBlock } = node
// 使用 Block 优化
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(), `)
}
// 调用创建函数
const callHelper = isBlock
? CREATE_ELEMENT_BLOCK
: CREATE_ELEMENT_VNODE
push(helper(callHelper) + `(`)
// 生成参数
const args: CallExpression['arguments'] = []
// tag
args.push(tag)
// props(动态属性对象)
if (props) {
args.push(props)
} else if (children || patchFlag) {
args.push('null')
}
// children
if (children) {
args.push(children)
} else if (patchFlag) {
args.push('null')
}
// patchFlag
if (patchFlag !== undefined) {
args.push(patchFlag + '')
}
// dynamicProps
if (dynamicProps) {
args.push(dynamicProps)
}
genNodeList(args, context)
push(')')
if (isBlock) {
push(')')
}
}
六、Render 函数生成示例
6.1 简单模板
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
编译结果:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", { class: "hello" }, [
_createElementVNode("h1", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
6.2 复杂模板
<template>
<div>
<p v-if="show">{{ text }}</p>
<ul v-else>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="toggle">切换</button>
</div>
</template>
编译结果:
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, renderSlot as _renderSlot } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_ctx.show
? (_openBlock(), _createElementBlock("p", { key: 0 }, _toDisplayString(_ctx.text), 1 /* TEXT */))
: (_openBlock(), _createElementBlock("ul", { key: 1 }, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("li", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */))
])),
_createElementVNode("button", {
onClick: _ctx.toggle
}, "切换", 8 /* PROPS */, ["onClick"])
]))
}
七、Mermaid 图表:编译器完整架构
八、代码示例:手写简单编译器
// 一个极简的模板编译器示例
function simpleCompile(template) {
// 1. 简单解析:提取标签、属性、文本
function parse(html) {
const tagRegex = /<([a-zA-Z][a-zA-Z0-9-]*)\s*([^>]*)>([\s\S]*?)<\/\1>/
const match = html.match(tagRegex)
if (!match) {
return { type: 'text', content: html.trim() }
}
const [, tag, attrStr, childrenStr] = match
const props = {}
// 解析属性
attrStr.replace(/([a-zA-Z-]+)="([^"]*)"/g, (_, key, val) => {
if (key.startsWith(':')) {
props[key.slice(1)] = { type: 'expression', content: val }
} else if (key.startsWith('@')) {
props['on' + key.slice(1).replace(/^\w/, c => c.toUpperCase())] =
{ type: 'expression', content: val }
} else {
props[key] = val
}
})
// 递归解析子节点
const children = []
const childRegex = /<(\w+)[\s\S]*?<\/\1>|{{\s*([^}]+)\s*}}|([^<]+)/g
let childMatch
while ((childMatch = childRegex.exec(childrenStr)) !== null) {
if (childMatch[1]) {
// 嵌套标签
const fullTag = childMatch[0]
children.push(parse(fullTag))
} else if (childMatch[2]) {
// 插值表达式
children.push({ type: 'interpolation', content: childMatch[2].trim() })
} else if (childMatch[3] && childMatch[3].trim()) {
// 文本
children.push({ type: 'text', content: childMatch[3].trim() })
}
}
return { type: 'element', tag, props, children }
}
// 2. 生成代码
function generate(node) {
if (node.type === 'text') {
return JSON.stringify(node.content)
}
if (node.type === 'interpolation') {
return `_toDisplayString(_ctx.${node.content})`
}
if (node.type === 'element') {
const tag = JSON.stringify(node.tag)
const props = generateProps(node.props)
const children = node.children.map(generate).join(', ')
return `_createElementVNode(${tag}, ${props}, [${children}])`
}
}
function generateProps(props) {
const entries = Object.entries(props)
if (entries.length === 0) return 'null'
const obj = entries.map(([key, val]) => {
if (typeof val === 'string') {
return `${key}: ${JSON.stringify(val)}`
} else {
return `${key}: _ctx.${val.content}`
}
}).join(', ')
return `{ ${obj} }`
}
const ast = parse(template)
const code = generate(ast)
return {
ast,
code: `function render(_ctx) { return ${code}; }`
}
}
// 测试
const template = `
<div class="app">
<h1>{{ title }}</h1>
<button @click="handleClick">点击</button>
</div>
`
const result = simpleCompile(template)
console.log(result.code)
// 输出:
// function render(_ctx) {
// return _createElementVNode("div", { class: "app" }, [
// _createElementVNode("h1", null, [_toDisplayString(_ctx.title)]),
// _createElementVNode("button", { onClick: _ctx.handleClick }, ["点击"])
// ]);
// }
九、常见问题
Q1:Vue3 编译器和 Vue2 编译器的主要区别?
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 架构 | 单体式 | 模块化(compiler-core + compiler-dom) |
| AST 节点 | 较简单 | 更丰富,支持更多节点类型 |
| 优化策略 | 运行时为主 | 编译时 + 运行时协同 |
| 静态提升 | 无 | 有 |
| PatchFlag | 无 | 有 |
| Block Tree | 无 | 有 |
| 代码生成 | 字符串拼接 | 结构化代码生成 |
Q2:编译时优化对性能的影响有多大?
在典型场景中,Vue3 编译优化带来的提升:
- 静态提升:减少 30%-50% 的 VNode 创建开销
- PatchFlag:减少 50%-80% 的属性对比时间
- Block Tree:减少 60%-90% 的 Diff 遍历时间
Q3:如何查看模板编译后的代码?
方法1:使用 Vue SFC Playground(https://sfc.vuejs.org/)
方法2:在项目中配置:
// vite.config.js
export default {
build: {
sourcemap: true
}
}
方法3:使用 @vue/compiler-sfc 手动编译:
import { compileTemplate } from '@vue/compiler-sfc'
const { code } = compileTemplate({
source: '<div>{{ msg }}</div>',
id: 'demo'
})
console.log(code)
Q4:编译器如何处理 TypeScript 类型?
Vue3 编译器本身不处理类型检查,类型检查由 vue-tsc 或 Vite 的插件完成。编译器只负责将模板转换为 JavaScript,TypeScript 类型信息在编译阶段被擦除。
十、总结
Vue 编译器是框架的核心基础设施,其工作流程可概括为:
- 解析(Parse):Tokenizer 将模板切分为 Token,Parser 构建 AST
- 转换(Transform):通过插件系统对 AST 进行转换和优化
- 生成(Generate):遍历优化后的 AST,生成渲染函数代码
关键优化手段:
- 静态提升:将静态节点提取到渲染函数外部
- PatchFlag:标记动态节点类型,运行时靶向更新
- Block Tree:分块追踪动态子节点
理解编译器原理,不仅能帮助你写出更高效的模板,还能在需要时手写渲染函数,或开发自定义的编译器插件。
十一、思考题
-
为什么 Vue 选择编译时优化而非纯运行时优化?两种方案的优劣分别是什么?
-
设计一个编译器插件,实现
v-permission自定义指令的编译时转换。 -
分析以下模板编译后的代码,指出哪些节点被静态提升、哪些有 PatchFlag:
<template> <div> <header>固定头部</header> <main :class="activeClass"> <p v-for="item in list" :key="item.id">{{ item.text }}</p> </main> <footer>版权所有 {{ year }}</footer> </div> </template> -
尝试扩展上面的
simpleCompile函数,支持v-if和v-for指令的编译。
更多推荐


所有评论(0)