Vue 3 编译器原理深入解析
更新时间:2025-02
目录
什么是 Vue 编译器
Vue 编译器负责将模板(Template)转换为渲染函数(Render Function),这个过程包括:
- 解析(Parse):将模板字符串解析为 AST(抽象语法树)
- 转换(Transform):对 AST 进行优化和转换
- 生成(Generate):将 AST 生成为渲染函数代码
编译流程图
模板字符串
↓
解析器(Parser)
↓
模板 AST
↓
转换器(Transformer)
↓
JavaScript AST
↓
代码生成器(Generator)
↓
渲染函数代码解析器(Parser)
基础实现
javascript
// 解析模板为 AST
function parse(template) {
const context = {
source: template,
mode: 'html',
advanceBy(num) {
context.source = context.source.sl
= parseInterpolation(context)
} else if (s[0] === '<') {
if (s[1] === '/') {
// 结束标签
continue
} else if (/[a-z]/i.test(s[1])) {
// 开始标签
node = parseElement(context, ancestors)
}
}
if (!node) {
// 解析文本
node = parseText(context)
}
nodes.push(node)
}
return nodes
}解析元素
javascript
function parseElement(context, ancestors) {
// 解析开始标签
const element = parseTag(context, 'start')
if (element.isSelfClosing) {
return element
}
// 递归解析子节点
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
// 解析结束标签
parseTag(context, 'end')
return element
}解析标签
javascript
function parseTag(context, type) {
const { advanceBy, advanceSpaces } = context
// 匹配标签名
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)
const tag = match[1]
advanceBy(match[0].length)
advanceSpaces()
// 解析属性
const props = parseAttributes(context)
// 检查是否自闭合
const isSelfClosing = context.source.startsWith('/>')
advanceBy(isSelfClosing ? 2 : 1)
return {
type: 'Element',
tag,
props,
children: [],
isSelfClosing
}
}解析属性
javascript
function parseAttributes(context) {
const { advanceBy, advanceSpaces } = context
const props = []
while (
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
) {
// 匹配属性名
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
const name = match[0]
advanceBy(name.length)
advanceSpaces(
ce(0, closeIndex - 2)
const content = rawContent.trim()
advanceBy(rawContent.length)
advanceBy(2) // 跳过 }}
return {
type: 'Interpolation',
content: {
type: 'Expression',
content
}
}
}解析文本
javascript
function parseText(context) {
// 找到文本结束位置
const endTokens = ['<', '{{']
let endIndex = context.source.length
for (const token of endTokens) {
const index = context.source.indexOf(token, 1)
if (index !== -1 && index < endIndex) {
endIndex = index
}
}
const content = context.source.slice(0, endIndex)
context.advanceBy(content.length)
return {
type: 'Text',
content
}
}转换器(Transformer)
基础实现
javascript
function transform(ast, options = {}) {
const context = {
currentNode: null,
parent: null,
childIndex: 0,
nodeTransforms: options.nodeTransforms || [],
replaceNode(node) {
context.parent.children[context.childIndex] = node
context.currentNode = node
},
removeNode() {
context.parent.children.splice(context.childIndex, 1)
context.currentNode = null
}
}
traverseNode(ast, context)
return ast
}遍历节点
javascript
function traverseNode(ast, context) {
context.currentNode = ast
// 应用转换插件(进入阶段)
const exitFns = []
const transforms = context.nodeTransforms
for (const transform of transforms) {
const onExit = transform(context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
// 节点可能被移除
if (!context.currentNode) return
}
// 递归处理子节点
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
// 应用转换插件(退出阶段)
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}转换插件示例
javascript
// 转换元素节点
function transformElement(node) {
return () => {
if (node.type !== 'Element') {
return
}
// 创建 JavaScript AST 节点
const callExp = {
type: 'CallExpression',
callee: 'h',
arguments: [
{ type: 'StringLiteral', value: node.tag },
node.props.length > 0 ? transformProps(node.props) : null,
node.children.length > 0 ? transformChildren(node.children) : null
].filter(Boolean)
}
node.jsNode = callExp
}
}
// 转换文本节点
function transformText(node) {
if (node.type !== 'Text') {
return
}
node.jsNode = {
type: 'StringLiteral',
value: node.content
}
}
// 转换插值节点
function transformInterpolation(node) {
if (node.type !== 'Interpolation') {
return
}
node.jsNode = {
type: 'CallExpression',
callee: 'toString',
arguments: [
{ type: 'Identifier', name: node.content.content }
]
}
}代码生成器(Generator)
基础实现
javascript
function generate(ast) {
const context = {
code: '',
push(code) {
context.code += code
},
indent() {
context.indentLevel++
},
deindent() {
context.indentLevel--
},
newline() {
context.code += '\n' + ' '.repeat(context.indentLevel)
},
indentLevel: 0
}
genFunctionPreamble(context)
context.push('return ')
if (ast.jsNode) {
genNode(ast.jsNode, context)
}
return context.code
}生成函数前导码
javascript
function genFunctionPreamble(context) {
const { push, newline } = context
push('function render() {')
newline()
push(' ')
}生成节点代码
javascript
function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl':
genFunctionDecl(node, context)
break
case 'ReturnStatement':
genReturnStatement(node, context)
break
case 'CallExpression':
genCallExpression(node, context)
break
case 'StringLiteral':
genStringLiteral(node, context)
break
case 'ArrayExpression':
genArrayExpression(node, context)
break
case 'Identifier':
genIdentifier(node, context)
break
}
}生成函数调用
javascript
function genCallExpression(node, context) {
const { push } = context
const { callee, arguments: args } = node
push(`${callee}(`)
for (let i = 0; i < args.length; i++) {
genNode(args[i], context)
if (i < args.length - 1) {
push(', ')
}
}
push(')')
}生成数组表达式
javascript
function genArrayExpression(node, context) {
const { push } = context
push('[')
for (let i = 0; i < node.elements.length; i++) {
genNode(node.elements[i], context)
if (i < node.elements.length - 1) {
push(', ')
}
}
push(']')
}完整编译示例
javascript
// 模板
const template = `
<div id="app">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
`
// 1. 解析
const ast = parse(template)
console.log(JSON.stringify(ast, null, 2))
// 2. 转换
transform(ast, {
nodeTransforms: [
transformElement,
transformText,
transformInterpolation
]
})
// 3. 生成
const code = generate(ast)
console.log(code)
// 输出的渲染函数:
// function render() {
// return h('div', { id: 'app' }, [
// h('h1', null, [toString(title)]),
// h('p', null, [toString(message)])
// ])
// }编译优化
1. 静态提升(Static Hoisting)
javascript
// 编译前
<div>
<p>静态文本</p>
<p>{{ dynamic }}</p>
</div>
// 编译后(优化)
const _hoisted_1 = h('p', null, '静态文本')
function render() {
return h('div', null, [
_hoisted_1, // 静态节点提升到外部
h('p', null, toString(dynamic))
])
}2. 预字符串化(Pre-Stringification)
javascript
// 编译前
<div>
<p>文本1</p>
<p>文本2</p>
<p>文本3</p>
</div>
// 编译后(优化)
const _hoisted_1 = createStaticVNode(
'<p>文本1</p><p>文本2</p><p>文本3</p>'
)
function render() {
return h('div', null, [_hoisted_1])
}3. 缓存事件处理函数
javascript
// 编译前
<button @click="handleClick">点击</button>
// 编译后(优化)
function render(_ctx, _cache) {
return h('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, '点击')
}4. Block Tree 优化
javascript
// 编译前
<div>
<p>{{ text }}</p>
</div>
// 编译后(优化)
function render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', null, toString(text), 1 /* TEXT */)
]))
}
// Block 收集动态节点,diff 时只比较动态节点指令编译
v-if 编译
javascript
// 模板
<div v-if="ok">Yes</div>
<div v-else>No</div>
// 编译后
function render() {
return ok
? h('div', null, 'Yes')
: h('div', null, 'No')
}v-for 编译
javascript
// 模板
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
// 编译后
function render() {
return renderList(items, (item) => {
return h('li', { key: item.id }, toString(item.text))
})
}v-model 编译
javascript
// 模板
<input v-model="text" />
// 编译后
function render() {
return h('input', {
value: text,
onInput: ($event) => {
text = $event.target.value
}
})
}v-bind 编译
javascript
// 模板
<div :class="{ active: isActive }" :style="{ color: textColor }">
内容
</div>
// 编译后
function render() {
return h('div', {
class: { active: isActive },
style: { color: textColor }
}, '内容')
}插槽编译
默认插槽
javascript
// 模板
<MyComponent>
<p>插槽内容</p>
</MyComponent>
// 编译后
function render() {
return h(MyComponent, null, {
default: () => [h('p', null, '插槽内容')]
})
}具名插槽
javascript
// 模板
<MyComponent>
<template #header>
<h1>标题</h1>
</template>
<template #default>
<p>内容</p>
</template>
</MyComponent>
// 编译后
function render() {
return h(MyComponent, null, {
header: () => [h('h1', null, '标题')],
default: () => [h('p', null, '内容')]
})
}作用域插槽
javascript
// 模板
<MyComponent>
<template #default="{ item }">
<p>{{ item.text }}</p>
</template>
</MyComponent>
// 编译后
function render() {
return h(MyComponent, null, {
default: ({ item }) => [h('p', null, toString(item.text))]
})
}性能优化标记
PatchFlags
javascript
// 动态文本
<div>{{ text }}</div>
// PatchFlag: 1 (TEXT)
// 动态 class
<div :class="cls"></div>
// PatchFlag: 2 (CLASS)
// 动态 style
<div :style="style"></div>
// PatchFlag: 4 (STYLE)
// 动态 props
<div :id="id"></div>
// PatchFlag: 8 (PROPS)
// 完整 props
<div v-bind="attrs"></div>
// PatchFlag: 16 (FULL_PROPS)ShapeFlags
javascript
// 元素节点
ShapeFlags.ELEMENT = 1
// 函数式组件
ShapeFlags.FUNCTIONAL_COMPONENT = 2
// 有状态组件
ShapeFlags.STATEFUL_COMPONENT = 4
// 文本子节点
ShapeFlags.TEXT_CHILDREN = 8
// 数组子节点
ShapeFlags.ARRAY_CHILDREN = 16
// 插槽子节点
ShapeFlags.SLOTS_CHILDREN = 32实战案例
1. 自定义编译器插件
javascript
// 自动添加 data-test 属性
function addTestIdPlugin() {
return (node) => {
if (node.type === 'Element') {
node.props.push({
type: 'Attribute',
name: 'data-test',
value: node.tag
})
}
}
}
// 使用
transform(ast, {
nodeTransforms: [
addTestIdPlugin(),
transformElement
]
})2. 编译时宏
javascript
// 定义宏
const macros = {
__DEV__: process.env.NODE_ENV !== 'production',
__VERSION__: '3.0.0'
}
// 替换宏
function replaceMacros(code) {
return code.replace(/__\w+__/g, (match) => {
return macros[match] !== undefined
? JSON.stringify(macros[match])
: match
})
}
// 使用
const code = generate(ast)
const finalCode = replaceMacros(code)常见问题
1. 为什么需要编译器?
- 提供更好的开发体验(模板语法)
- 编译时优化(静态提升、预字符串化)
- 更好的错误提示
2. 运行时编译 vs 构建时编译?
javascript
// 运行时编译(体积大,性能差)
import { createApp } from 'vue'
createApp({
template: '<div>{{ msg }}</div>',
data() {
return { msg: 'Hello' }
}
})
// 构建时编译(体积小,性能好)
import { createApp, h } from 'vue'
createApp({
render() {
return h('div', this.msg)
},
data() {
return { msg: 'Hello' }
}
})3. 如何调试编译结果?
javascript
// 使用 @vue/compiler-sfc
import { compile } from '@vue/compiler-sfc'
const { code } = compile(`
<template>
<div>{{ msg }}</div>
</template>
`)
console.log(code)面试要点
核心问题
Vue 编译器的三个阶段是什么?
- 解析(Parse):模板 → AST
- 转换(Transform):AST 优化
- 生成(Generate):AST → 代码
Vue 3 编译器有哪些优化?
- 静态提升
- 预字符串化
- 缓存事件处理函数
- Block Tree
- PatchFlags
v-if 和 v-for 如何编译?
- v-if 编译为三元表达式
- v-for 编译为 renderList 函数调用
追问点
- 编译器如何处理指令?
- 插槽如何编译?
- 如何实现编译时优化?
- 运行时编译和构建时编译的区别?