Vue 模板编译原理
理解 Vue 模板编译的三个阶段:Parse、Optimize、Generate
问题
Vue 模板是如何编译成渲染函数的?解释 Parse、Optimize、Generate 三个阶段的作用。
解答
Vue 模板编译分为三个阶段:
模板字符串 → Parse → AST → Optimize → 优化后的AST → Generate → render函数
1. Parse(解析阶段)
将模板字符串解析成抽象语法树(AST)。
// 模板
const template = `<div id="app">{{ message }}</div>`
// 解析后的 AST 结构
const ast = {
tag: 'div',
type: 1, // 1: 元素节点
attrs: [{ name: 'id', value: 'app' }],
children: [
{
type: 2, // 2: 带表达式的文本节点
expression: '_s(message)', // _s 是 toString 方法
text: '{{ message }}'
}
]
}
简化版解析器实现:
function parse(template) {
// 匹配开始标签
const startTagRE = /^<([a-zA-Z]+)([^>]*)>/
// 匹配结束标签
const endTagRE = /^<\/([a-zA-Z]+)>/
// 匹配插值表达式
const interpolationRE = /\{\{(.+?)\}\}/g
let index = 0
const root = { children: [] }
const stack = [root]
while (index < template.length) {
const current = stack[stack.length - 1]
const rest = template.slice(index)
if (rest.startsWith('</')) {
// 处理结束标签
const match = rest.match(endTagRE)
if (match) {
stack.pop()
index += match[0].length
}
} else if (rest.startsWith('<')) {
// 处理开始标签
const match = rest.match(startTagRE)
if (match) {
const node = {
tag: match[1],
type: 1,
children: [],
attrs: parseAttrs(match[2])
}
current.children.push(node)
stack.push(node)
index += match[0].length
}
} else {
// 处理文本内容
const endIndex = rest.indexOf('<')
const text = endIndex === -1 ? rest : rest.slice(0, endIndex)
if (text.trim()) {
current.children.push({
type: interpolationRE.test(text) ? 2 : 3,
text: text.trim()
})
}
index += text.length
}
}
return root.children[0]
}
function parseAttrs(attrStr) {
const attrs = []
const attrRE = /([a-zA-Z-]+)="([^"]*)"/g
let match
while ((match = attrRE.exec(attrStr))) {
attrs.push({ name: match[1], value: match[2] })
}
return attrs
}
2. Optimize(优化阶段)
标记静态节点和静态根节点,在后续更新时跳过这些节点的 diff 比较。
function optimize(node) {
// 标记是否为静态节点
markStatic(node)
// 标记静态根节点
markStaticRoot(node)
}
function markStatic(node) {
node.static = isStatic(node)
if (node.type === 1) {
// 递归标记子节点
for (const child of node.children) {
markStatic(child)
// 子节点非静态,父节点也非静态
if (!child.static) {
node.static = false
}
}
}
}
function isStatic(node) {
// 带表达式的文本节点不是静态的
if (node.type === 2) return false
// 纯文本节点是静态的
if (node.type === 3) return true
// 元素节点需要检查是否有动态绑定
return !node.attrs?.some(attr =>
attr.name.startsWith(':') ||
attr.name.startsWith('@') ||
attr.name.startsWith('v-')
)
}
function markStaticRoot(node) {
if (node.type === 1) {
// 静态根节点:本身是静态的,且有子节点,且子节点不只是一个纯文本节点
if (node.static && node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)) {
node.staticRoot = true
return
}
node.staticRoot = false
// 递归处理子节点
for (const child of node.children) {
markStaticRoot(child)
}
}
}
3. Generate(生成阶段)
将 AST 转换成 render 函数字符串。
function generate(node) {
const code = genElement(node)
return {
render: `with(this) { return ${code} }`
}
}
function genElement(node) {
if (node.type === 1) {
// 元素节点
const tag = `'${node.tag}'`
const data = genData(node)
const children = genChildren(node)
return `_c(${tag}${data ? `,${data}` : ''}${children ? `,${children}` : ''})`
} else if (node.type === 2) {
// 带表达式的文本
return `_v(_s(${node.text.replace(/\{\{(.+?)\}\}/g, '$1').trim()}))`
} else {
// 纯文本
return `_v('${node.text}')`
}
}
function genData(node) {
if (!node.attrs?.length) return ''
const attrs = node.attrs.map(a => `${a.name}:"${a.value}"`).join(',')
return `{attrs:{${attrs}}}`
}
function genChildren(node) {
if (!node.children?.length) return ''
return `[${node.children.map(genElement).join(',')}]`
}
完整示例
// 编译入口
function compile(template) {
// 1. 解析模板生成 AST
const ast = parse(template)
// 2. 优化 AST
optimize(ast)
// 3. 生成 render 函数
const code = generate(ast)
return code
}
// 测试
const template = `<div id="app"><span>Hello</span><p>{{ name }}</p></div>`
const result = compile(template)
console.log(result.render)
// with(this) { return _c('div',{attrs:{id:"app"}},[_c('span',,[_v('Hello')]),_c('p',,[_v(_s(name))])]) }
render 函数中的辅助方法:
// Vue 内部的渲染辅助方法
_c = createElement // 创建元素 VNode
_v = createTextVNode // 创建文本 VNode
_s = toString // 转字符串
关键点
- Parse:通过正则和状态机将模板解析成 AST,记录标签、属性、指令等信息
- Optimize:标记静态节点,patch 时直接跳过,减少 diff 开销
- Generate:递归遍历 AST,拼接成 render 函数代码字符串
- 编译可以在构建时完成(vue-loader),也可以在运行时完成(完整版 Vue)
- 静态节点只会渲染一次,后续更新时复用之前的 VNode
目录