Vue 编译器核心原理详解
Vue 的模板语法(<template>)之所以能变成浏览器可执行的 JavaScript 代码,靠的就是 编译器(Compiler)。本文将从整体架构到每个阶段的实现细节,带你彻底搞懂 Vue 编译器是如何工作的。
1. 从一个问题开始
我们平时写的 Vue 模板是这样的:
<template>
<div id="app">
<p>{{ message }}</p>
<span v-if="show">可见</span>
</div>
</template>
但浏览器并不认识 {{ }}、v-if 这些语法。那么 Vue 是怎样把模板变成浏览器能执行的渲染函数的呢?
答案就是——编译器。Vue 编译器会在构建阶段(或运行时)将模板字符串编译为一个 render 函数:
// 编译后的结果(简化版)
function render(_ctx) {
return h('div', { id: 'app' }, [
h('p', null, _ctx.message),
_ctx.show ? h('span', null, '可见') : null
])
}
这个从模板到渲染函数的过程,就是编译器要做的事情。
2. 编译器的整体架构
Vue 3 的编译器采用了经典的三阶段编译架构,与大多数编程语言的编译器设计类似:
| 阶段 | 输入 | 输出 | 职责 |
|---|---|---|---|
| Parse(解析) | 模板字符串 | AST(抽象语法树) | 词法分析 + 语法分析,将模板转为树形结构 |
| Transform(转换) | AST | 优化后的 AST | 遍历 AST,执行静态分析和节点转换 |
| Generate(生成) | 优化后的 AST | 渲染函数字符串 | 将 AST 拼接为可执行的 JavaScript 代码 |
这三个阶段各自独立、职责分明,让编译器的逻辑清晰且易于扩展。
2.1 编译器在 Vue 中的位置
Vue 3 的编译器相关包包括:
@vue/compiler-core:与平台无关的编译器核心,包含 Parse、Transform、Generate 三大阶段@vue/compiler-dom:基于 core,针对浏览器 DOM 平台扩展(处理v-html、v-model等 DOM 特有指令)@vue/compiler-sfc:单文件组件(.vue文件)编译器,处理<template>、<script>、<style>三个块@vue/compiler-ssr:服务端渲染编译器
3. 第一阶段:解析(Parse)
解析阶段的任务是将模板字符串转换为 AST(抽象语法树)。
3.1 什么是 AST
AST(Abstract Syntax Tree,抽象语法树)是源代码的树状表示形式。它将模板中的每一个标签、属性、文本都抽象为一个节点对象。
以这段模板为例:
<div id="app">
<p>{{ message }}</p>
</div>
解析后的 AST 结构大致如下:
// 简化的 AST 结构
{
type: 0, // ROOT
children: [
{
type: 1, // ELEMENT
tag: 'div',
props: [
{
type: 6, // ATTRIBUTE
name: 'id',
value: { content: 'app' }
}
],
children: [
{
type: 1, // ELEMENT
tag: 'p',
children: [
{
type: 5, // INTERPOLATION(插值表达式)
content: {
type: 4, // SIMPLE_EXPRESSION
content: 'message'
}
}
]
}
]
}
]
}
对应的树形结构:
3.2 Vue AST 节点类型
Vue 编译器定义了丰富的节点类型来描述模板中的各种元素:
// @vue/compiler-core 中的节点类型(部分)
const NodeTypes = {
ROOT: 0, // 根节点
ELEMENT: 1, // 元素节点 <div>
TEXT: 2, // 纯文本节点
COMMENT: 3, // 注释节点 <!-- -->
SIMPLE_EXPRESSION: 4, // 简单表达式 message
INTERPOLATION: 5, // 插值表达式 {{ }}
ATTRIBUTE: 6, // 静态属性 id="app"
DIRECTIVE: 7, // 指令 v-if / v-for / @click
COMPOUND_EXPRESSION: 8, // 复合表达式
IF: 9, // v-if 节点
IF_BRANCH: 10, // v-if 分支
FOR: 11, // v-for 节点
TEXT_CALL: 12, // 文本函数调用
VNODE_CALL: 13, // VNode 调用
JS_CALL_EXPRESSION: 14, // JS 函数调用表达式
// ... 更多类型
}
3.3 解析器的工作原理
Vue 的解析器采用有限状态机的方式逐字符扫描模板字符串。核心思想是维护一个游标(cursor),逐步推进,根据当前字符判断进入不同的解析状态。
3.4 解析流程的核心代码
下面是 Vue 3 解析器的简化版实现,帮助理解核心逻辑:
// 简化版 parse 函数
function parse(template) {
// 创建解析上下文
const context = {
source: template, // 待解析的模板字符串
offset: 0, // 当前解析位置
line: 1, // 当前行号
column: 1 // 当前列号
}
// 解析子节点,返回 AST 的 children
const children = parseChildren(context, [])
// 返回根节点
return {
type: 0, // ROOT
children,
loc: getLoc(context, 0)
}
}
function parseChildren(context, ancestors) {
const nodes = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node
if (s.startsWith('{{')) {
// 解析插值表达式 {{ message }}
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (s[1] === '/') {
// 结束标签,交给父级处理
break
} else if (/[a-z]/i.test(s[1])) {
// 解析元素标签 <div>
node = parseElement(context, ancestors)
}
}
// 如果以上都不匹配,当做文本解析
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
3.5 解析元素的详细过程
function parseElement(context, ancestors) {
// 1. 解析开始标签 <div id="app">
const element = parseTag(context) // { tag: 'div', props: [...] }
// 自闭合标签(如 <br/>)直接返回
if (element.isSelfClosing) {
return element
}
// 2. 递归解析子节点
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
// 3. 解析结束标签 </div>
if (context.source.startsWith(`</${element.tag}`)) {
parseTag(context) // 消费结束标签
} else {
console.error(`缺少结束标签: </${element.tag}>`)
}
return element
}
3.6 解析插值表达式
function parseInterpolation(context) {
// {{ message }} => 提取 "message"
const openDelimiter = '{{'
const closeDelimiter = '}}'
// 找到结束定界符的位置
const closeIndex = context.source.indexOf(
closeDelimiter,
openDelimiter.length
)
// 推进游标跳过 {{
advanceBy(context, openDelimiter.length)
// 提取中间的表达式内容
const rawContentLength = closeIndex - openDelimiter.length
const rawContent = context.source.slice(0, rawContentLength)
const content = rawContent.trim()
// 推进游标跳过内容和 }}
advanceBy(context, rawContentLength + closeDelimiter.length)
return {
type: 5, // INTERPOLATION
content: {
type: 4, // SIMPLE_EXPRESSION
content
}
}
}
3.7 解析指令
指令是 Vue 模板的核心语法。解析器需要识别 v-if、v-for、@click、:class 等多种语法糖:
function parseDirective(name, value) {
// v-on:click.prevent => { name: 'on', arg: 'click', modifiers: ['prevent'] }
// @click.prevent => { name: 'on', arg: 'click', modifiers: ['prevent'] }
// :class => { name: 'bind', arg: 'class', modifiers: [] }
// v-model.trim => { name: 'model', arg: undefined, modifiers: ['trim'] }
// v-slot:header => { name: 'slot', arg: 'header', modifiers: [] }
let dirName, arg, modifiers
if (name.startsWith('@')) {
// @click => v-on:click 的简写
dirName = 'on'
name = name.slice(1)
} else if (name.startsWith(':')) {
// :class => v-bind:class 的简写
dirName = 'bind'
name = name.slice(1)
} else if (name.startsWith('#')) {
// #header => v-slot:header 的简写
dirName = 'slot'
name = name.slice(1)
} else {
// v-if / v-for / v-model 等
dirName = name.slice(2) // 去掉 v- 前缀
const dotIndex = dirName.indexOf('.')
if (dotIndex > 0) {
name = dirName.slice(dotIndex + 1)
dirName = dirName.slice(0, dotIndex)
} else {
name = ''
}
}
// 解析参数和修饰符
const parts = name.split('.')
arg = parts[0] || undefined
modifiers = parts.slice(1)
return {
type: 7, // DIRECTIVE
name: dirName,
arg,
modifiers,
exp: value ? { type: 4, content: value } : undefined
}
}
4. 第二阶段:转换(Transform)
转换阶段的任务是遍历 AST 并对节点进行分析和改写。这是编译器中最复杂、也最关键的一个阶段——Vue 3 的编译优化主要就发生在这里。
4.1 转换的基本思路
转换阶段使用访问者模式(Visitor Pattern)——定义一组「转换插件」,然后深度优先遍历 AST,对每个节点依次调用这些插件。
4.2 转换器的核心代码
function transform(root, options) {
// 创建转换上下文
const context = {
root,
helpers: new Map(), // 收集运行时帮助函数
components: new Set(), // 收集使用到的组件
directives: new Set(), // 收集使用到的指令
hoists: [], // 收集可以静态提升的节点
currentNode: root, // 当前正在处理的节点
parent: null, // 父节点
childIndex: 0, // 当前节点在父节点中的索引
// 转换插件(根据编译目标不同而不同)
nodeTransforms: options.nodeTransforms || [],
directiveTransforms: options.directiveTransforms || {}
}
// 从根节点开始遍历
traverseNode(root, context)
// 执行静态提升
if (options.hoistStatic) {
hoistStatic(root, context)
}
// 创建根节点的代码生成节点
createRootCodegen(root, context)
}
function traverseNode(node, context) {
context.currentNode = node
const { nodeTransforms } = context
const exitFns = [] // 收集退出回调
// === 进入阶段 ===
for (let i = 0; i < nodeTransforms.length; i++) {
// 每个插件可以返回一个退出回调
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (Array.isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
// 插件可能已经移除了当前节点
if (!context.currentNode) return
node = context.currentNode
}
// === 递归子节点 ===
switch (node.type) {
case 0: // ROOT
case 1: // ELEMENT
case 11: // FOR
case 9: // IF
traverseChildren(node, context)
break
}
// === 退出阶段(逆序调用)===
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
进入阶段处理的是「自顶向下」的逻辑(如判断节点类型、收集信息),退出阶段处理的是「自底向上」的逻辑(如根据子节点信息生成当前节点的代码)。这样当退出回调执行时,子节点都已经处理完毕,可以安全地使用子节点的转换结果。
4.3 关键转换插件解析
transformIf —— 处理 v-if / v-else-if / v-else
v-if 指令在编译阶段会被转换为条件分支结构:
<!-- 模板 -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
经过 transformIf 转换后,AST 中会生成一个 IF 节点:
// 转换后的 AST 结构(简化)
{
type: 9, // IF
branches: [
{
type: 10, // IF_BRANCH
condition: { content: "type === 'A'" },
children: [{ type: 1, tag: 'div', children: [{ type: 2, content: 'A' }] }]
},
{
type: 10,
condition: { content: "type === 'B'" },
children: [{ type: 1, tag: 'div', children: [{ type: 2, content: 'B' }] }]
},
{
type: 10,
condition: undefined, // else 分支无条件
children: [{ type: 1, tag: 'div', children: [{ type: 2, content: 'C' }] }]
}
]
}
transformFor —— 处理 v-for
<li v-for="(item, index) in list" :key="item.id">
{{ item.name }}
</li>
转换后生成 FOR 节点:
{
type: 11, // FOR
source: { content: 'list' }, // 遍历的数据源
valueAlias: { content: 'item' }, // 值别名
keyAlias: { content: 'index' }, // 索引别名
children: [/* 循环体节点 */]
}
transformElement —— 处理元素节点
这是最核心的转换插件之一,负责将元素节点转换为 createVNode 调用描述:
// 转换前
{ type: 1, tag: 'div', props: [...], children: [...] }
// 转换后(简化)—— 在退出阶段生成 codegenNode
{
type: 13, // VNODE_CALL
tag: '"div"',
props: { /* 属性对象 */ },
children: { /* 子节点 */ },
patchFlag: 1, // 补丁标记(优化关键!)
dynamicProps: ['class']
}
4.4 补丁标记(PatchFlag)
PatchFlag 是 Vue 3 编译优化的核心机制之一。编译器在 Transform 阶段分析节点的动态绑定,为每个 VNode 添加一个数字标记,告诉运行时「这个节点的哪些部分是动态的」。
// PatchFlag 枚举值
const PatchFlags = {
TEXT: 1, // 动态文本内容 {{ msg }}
CLASS: 2, // 动态 class :class="cls"
STYLE: 4, // 动态 style :style="stl"
PROPS: 8, // 动态非 class/style 的 prop
FULL_PROPS: 16, // 具有动态 key 的 prop(需全量 diff)
NEED_HYDRATION: 32, // 需要 hydration 的事件监听
STABLE_FRAGMENT: 64, // 子节点顺序不会变的 Fragment
KEYED_FRAGMENT: 128, // 带 key 的 Fragment
UNKEYED_FRAGMENT: 256,// 不带 key 的 Fragment
NEED_PATCH: 512, // 需要 patch(非 prop 的绑定如 ref、指令)
DYNAMIC_SLOTS: 1024, // 动态插槽
HOISTED: -1, // 静态提升节点(不需要 patch)
BAIL: -2 // 退出优化模式
}
来看一个具体的例子:
<div>
<p class="static">静态文本</p>
<p :class="dynamicClass">{{ dynamicText }}</p>
<p :id="dynamicId" :title="dynamicTitle">固定文本</p>
</div>
编译器为每个节点生成的 PatchFlag:
// 编译结果(简化)
function render(_ctx) {
return h('div', null, [
// 静态节点:无 PatchFlag,可以被静态提升
h('p', { class: 'static' }, '静态文本'),
// PatchFlag = 3(TEXT | CLASS)
// 运行时只需对比 textContent 和 class
h('p', { class: _ctx.dynamicClass }, _ctx.dynamicText, 3 /* TEXT | CLASS */),
// PatchFlag = 8(PROPS),dynamicProps: ["id", "title"]
// 运行时只需对比 id 和 title 属性
h('p', {
id: _ctx.dynamicId,
title: _ctx.dynamicTitle
}, '固定文本', 8 /* PROPS */, ["id", "title"])
])
}
PatchFlag 本质上是编译器给运行时的「提示」——把模板编译阶段就能确定的信息传递给运行时,让运行时在 patch 阶段做最少的工作。这是一种编译时优化换运行时性能的典型策略。
4.5 静态提升(Static Hoisting)
静态提升是 Vue 3 编译器的另一项重要优化。对于完全静态的节点(没有任何动态绑定),编译器会将它们提升到渲染函数外部,避免每次重新渲染时重复创建。
<div>
<p class="title">欢迎来到首页</p>
<p class="desc">这是一段固定描述</p>
<p>{{ dynamicMessage }}</p>
</div>
未开启静态提升:
function render(_ctx) {
return h('div', null, [
// 每次渲染都会重新创建这两个静态 VNode
h('p', { class: 'title' }, '欢迎来到首页'),
h('p', { class: 'desc' }, '这是一段固定描述'),
h('p', null, _ctx.dynamicMessage, 1 /* TEXT */)
])
}
开启静态提升后:
// 提升到模块作用域,只创建一次
const _hoisted_1 = h('p', { class: 'title' }, '欢迎来到首页')
const _hoisted_2 = h('p', { class: 'desc' }, '这是一段固定描述')
function render(_ctx) {
return h('div', null, [
_hoisted_1, // 直接复用
_hoisted_2, // 直接复用
h('p', null, _ctx.dynamicMessage, 1 /* TEXT */)
])
}
静态提升不仅适用于元素节点,还适用于静态属性对象:
<div id="container" class="wrapper">
{{ message }}
</div>
// 属性对象也会被提升
const _hoisted_props = { id: 'container', class: 'wrapper' }
function render(_ctx) {
return h('div', _hoisted_props, _ctx.message, 1 /* TEXT */)
}
4.6 Block Tree 与动态节点收集
为了进一步优化 Diff 性能,Vue 3 引入了 Block 的概念。Block 是一个特殊的 VNode,它会收集其所有后代中的动态节点到一个扁平数组 dynamicChildren 中。
// Block 的运行时表示
const block = {
type: 'div',
children: [/* 完整的子树 */],
// 额外收集所有动态后代节点(扁平数组)
dynamicChildren: [
{ type: 'p', children: ctx.msg, patchFlag: 1 },
{ type: 'a', props: { href: ctx.url }, patchFlag: 8 }
]
}
运行时 patch 时,对于 Block 节点只需遍历 dynamicChildren,跳过整棵子树中所有的静态节点,实现靶向更新。
并非所有节点都在同一个 Block 中。带有结构性指令(v-if、v-for)的节点会创建新的 Block,因为它们可能改变子树的结构,导致 dynamicChildren 的对应关系发生变化。
4.7 缓存事件处理函数(Cache Handlers)
编译器还会对内联事件处理函数进行缓存优化:
<button @click="count++">增加</button>
// 未缓存:每次渲染都创建新的函数对象
function render(_ctx) {
return h('button', {
onClick: ($event) => (_ctx.count++)
}, '增加')
}
// 缓存后:使用缓存数组,避免创建新函数导致子组件不必要的更新
function render(_ctx, _cache) {
return h('button', {
onClick: _cache[0] || (_cache[0] = ($event) => (_ctx.count++))
}, '增加')
}
这样做的好处是:当父组件重新渲染时,传给子组件的回调函数引用不变,可以避免子组件不必要的重新渲染。
4.8 预字符串化(Pre-Stringification)
当模板中出现大量连续的静态节点时,编译器会将它们序列化为一个纯 HTML 字符串,在运行时通过 innerHTML 一次性创建:
<div>
<p>段落1</p>
<p>段落2</p>
<p>段落3</p>
<p>段落4</p>
<p>段落5</p>
<!-- 假设有很多连续的静态节点 -->
</div>
// 预字符串化后
const _hoisted = createStaticVNode(
'<p>段落1</p><p>段落2</p><p>段落3</p><p>段落4</p><p>段落5</p>'
)
这比逐个创建 VNode 更高效,减少了对象创建的开销。
5. 第三阶段:代码生成(Generate)
代码生成阶段的任务是将转换后的 AST 转换为可执行的 JavaScript 代码字符串。
5.1 代码生成的目标
5.2 代码生成器的核心逻辑
function generate(ast, options) {
const context = createCodegenContext(ast, options)
const { push, indent, deindent, newline } = context
// 1. 生成前导代码(import 语句)
genFunctionPreamble(ast, context)
// 2. 生成函数签名
const functionName = 'render'
const args = ['_ctx', '_cache']
push(`function ${functionName}(${args.join(', ')}) {`)
indent()
// 3. 生成 return 语句
push('return ')
// 4. 递归生成节点代码
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push('null')
}
// 5. 闭合函数
deindent()
push('}')
return {
code: context.code,
ast
}
}
5.3 节点代码生成
不同类型的节点有不同的生成逻辑:
function genNode(node, context) {
switch (node.type) {
case 13: // VNODE_CALL
genVNodeCall(node, context)
break
case 2: // TEXT
genText(node, context)
break
case 4: // SIMPLE_EXPRESSION
genExpression(node, context)
break
case 5: // INTERPOLATION
genInterpolation(node, context)
break
case 8: // COMPOUND_EXPRESSION
genCompoundExpression(node, context)
break
case 14: // JS_CALL_EXPRESSION
genCallExpression(node, context)
break
case 9: // IF
genIfNode(node, context)
break
case 11: // FOR
genForNode(node, context)
break
// ... 更多类型
}
}
// 生成 VNode 调用
function genVNodeCall(node, context) {
const { push, helper } = context
const { tag, props, children, patchFlag, dynamicProps } = node
// createElementVNode("div", { id: "app" }, children, 1)
push(helper('createElementVNode') + '(')
genNodeList(
[tag, props, children, patchFlag, dynamicProps],
context
)
push(')')
}
5.4 完整编译示例
来看一个从模板到最终代码的完整示例:
输入模板:
<div id="app">
<h1>标题</h1>
<p :class="pClass">{{ message }}</p>
<button @click="handleClick">点击</button>
</div>
编译输出(简化版):
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"
// 静态提升
const _hoisted_1 = { id: "app" }
const _hoisted_2 = _createElementVNode("h1", null, "标题", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("p", {
class: _ctx.pClass
}, _toDisplayString(_ctx.message), 3 /* TEXT, CLASS */),
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "点击")
]))
}
6. Vue 2 与 Vue 3 编译器对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| AST 结构 | 自定义 AST | 更规范的 AST,支持更多节点类型 |
| 静态标记 | 仅标记静态根节点 | PatchFlag 精确标记每个动态绑定 |
| 静态提升 | 不支持 | 支持静态节点和属性提升 |
| 事件缓存 | 不支持 | 支持 cacheHandlers |
| Block Tree | 不支持 | 支持 Block + dynamicChildren |
| 预字符串化 | 不支持 | 大量连续静态节点预字符串化 |
| Tree-shaking | 不支持,全量引入 | 按需引入运行时 helper |
| 编译目标 | 仅浏览器 | 支持 DOM、SSR 等多平台 |
| 源码语言 | JavaScript(Flow 类型) | TypeScript |
| 架构 | 单一包 | 分层:core → dom → sfc |
6.1 Vue 2 的静态优化
Vue 2 的编译器只做了一件优化——标记静态子树(markStatic):
// Vue 2:只标记整棵静态子树
// 如果一个节点的所有后代都是静态的,则标记为静态根节点
function markStaticRoots(node) {
if (node.static && node.children.length > 0) {
node.staticRoot = true
// 静态根节点在 patch 时会被跳过
return
}
// 只要有一个子节点是动态的,整棵子树都不是静态根
}
这意味着哪怕只有一个子节点是动态的,整棵子树的所有兄弟节点都无法享受静态优化。
6.2 Vue 3 的编译优化总结
7. 手写简易 Vue 编译器
为了加深理解,让我们从零实现一个支持基本功能的简易 Vue 编译器。
7.1 实现目标
将简单模板编译为渲染函数:
<!-- 输入 -->
<div id="app">
<p>{{ message }}</p>
<span>静态文本</span>
</div>
<!-- 输出 -->
function render(ctx) {
return h('div', { id: 'app' }, [
h('p', null, [ctx.message]),
h('span', null, ['静态文本'])
])
}
7.2 完整实现
// ==================== 第一步:Parse ====================
// AST 节点类型
const NodeType = {
Root: 'Root',
Element: 'Element',
Text: 'Text',
Interpolation: 'Interpolation'
}
function parse(template) {
const context = {
source: template.trim(),
// 前进 n 个字符
advance(n) {
this.source = this.source.slice(n)
}
}
const children = parseChildren(context)
return { type: NodeType.Root, children }
}
function parseChildren(context) {
const nodes = []
while (context.source.length > 0) {
// 遇到结束标签,停止
if (context.source.startsWith('</')) break
let node
if (context.source.startsWith('{{')) {
node = parseInterpolation(context)
} else if (context.source[0] === '<') {
node = parseElement(context)
} else {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function parseInterpolation(context) {
context.advance(2) // 跳过 {{
const closeIndex = context.source.indexOf('}}')
const content = context.source.slice(0, closeIndex).trim()
context.advance(closeIndex + 2) // 跳过内容和 }}
return {
type: NodeType.Interpolation,
content
}
}
function parseElement(context) {
// 解析开始标签 <div id="app">
const tagMatch = context.source.match(/^<([a-z][a-z0-9]*)/i)
const tag = tagMatch[1]
context.advance(tagMatch[0].length)
// 解析属性
const props = parseAttributes(context)
// 跳过 > 或 />
const isSelfClosing = context.source.startsWith('/>')
context.advance(isSelfClosing ? 2 : 1)
if (isSelfClosing) {
return { type: NodeType.Element, tag, props, children: [] }
}
// 递归解析子节点
const children = parseChildren(context)
// 消费结束标签 </div>
const endTagMatch = context.source.match(new RegExp(`^</${tag}\\s*>`))
if (endTagMatch) {
context.advance(endTagMatch[0].length)
}
return { type: NodeType.Element, tag, props, children }
}
function parseAttributes(context) {
const props = []
// 持续解析属性直到遇到 > 或 />
while (
context.source.length > 0 &&
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
) {
// 跳过空白
const spaceMatch = context.source.match(/^\s+/)
if (spaceMatch) context.advance(spaceMatch[0].length)
if (context.source.startsWith('>') || context.source.startsWith('/>')) break
// 匹配 key="value" 或 key='value'
const attrMatch = context.source.match(/^([a-z_@:][a-z0-9_-]*)(?:\s*=\s*"([^"]*)")?/i)
if (attrMatch) {
props.push({ name: attrMatch[1], value: attrMatch[2] ?? true })
context.advance(attrMatch[0].length)
}
}
return props
}
function parseText(context) {
// 文本的结束位置:< 或 {{ 之前
const endTokens = ['<', '{{']
let endIndex = context.source.length
for (const token of endTokens) {
const idx = context.source.indexOf(token)
if (idx !== -1 && idx < endIndex) {
endIndex = idx
}
}
const content = context.source.slice(0, endIndex)
context.advance(endIndex)
return { type: NodeType.Text, content }
}
// ==================== 第二步:Transform ====================
function transform(ast) {
traverseNode(ast)
}
function traverseNode(node) {
// 处理子节点
const children = node.children || []
for (let i = 0; i < children.length; i++) {
traverseNode(children[i])
}
// 为节点生成 codegenNode(简化版)
switch (node.type) {
case NodeType.Root:
node.codegenNode = node.children
break
case NodeType.Element:
// 标记是否为纯静态节点
node.isStatic = isStaticNode(node)
break
case NodeType.Interpolation:
node.isStatic = false
break
case NodeType.Text:
node.isStatic = true
break
}
}
function isStaticNode(node) {
if (node.type === NodeType.Interpolation) return false
if (node.type === NodeType.Text) return true
if (node.type === NodeType.Element) {
return node.children.every(child => isStaticNode(child))
}
return false
}
// ==================== 第三步:Generate ====================
function generate(ast) {
const code = genNode(ast)
return `function render(ctx) {\n return ${code}\n}`
}
function genNode(node) {
switch (node.type) {
case NodeType.Root:
// 根节点只有一个子元素时直接返回,否则返回数组
return node.children.length === 1
? genNode(node.children[0])
: `[${node.children.map(c => genNode(c)).join(', ')}]`
case NodeType.Element:
return genElement(node)
case NodeType.Text:
return JSON.stringify(node.content)
case NodeType.Interpolation:
return `ctx.${node.content}`
}
}
function genElement(node) {
const tag = JSON.stringify(node.tag)
// 生成 props
let props = 'null'
if (node.props.length > 0) {
const propsStr = node.props
.map(p => `${JSON.stringify(p.name)}: ${JSON.stringify(p.value)}`)
.join(', ')
props = `{ ${propsStr} }`
}
// 生成 children
let children = 'null'
if (node.children.length > 0) {
const childrenCode = node.children.map(c => genNode(c))
children = `[${childrenCode.join(', ')}]`
}
return `h(${tag}, ${props}, ${children})`
}
// ==================== 使用示例 ====================
const template = `<div id="app"><p>{{ message }}</p><span>静态文本</span></div>`
// 1. Parse:模板 → AST
const ast = parse(template)
console.log('=== AST ===')
console.log(JSON.stringify(ast, null, 2))
// 2. Transform:AST 优化
transform(ast)
// 3. Generate:AST → 代码
const code = generate(ast)
console.log('\n=== 生成的代码 ===')
console.log(code)
// 输出:
// function render(ctx) {
// return h("div", { "id": "app" }, [h("p", null, [ctx.message]), h("span", null, ["静态文本"])])
// }
7.3 运行验证
// 配合简易的 h 函数和响应式系统使用
function h(tag, props, children) {
return { tag, props, children }
}
// 通过 new Function 将代码字符串转为可执行函数
const renderFn = new Function('ctx', 'h', `
return h("div", { "id": "app" }, [
h("p", null, [ctx.message]),
h("span", null, ["静态文本"])
])
`)
const vnode = renderFn({ message: 'Hello Vue!' }, h)
console.log(JSON.stringify(vnode, null, 2))
// {
// "tag": "div",
// "props": { "id": "app" },
// "children": [
// { "tag": "p", "props": null, "children": ["Hello Vue!"] },
// { "tag": "span", "props": null, "children": ["静态文本"] }
// ]
// }
8. 编译器在实际开发中的应用
8.1 运行时编译 vs 预编译
| 对比项 | 运行时编译 | 预编译 |
|---|---|---|
| 编译时机 | 浏览器中运行时 | 构建阶段(Node.js) |
| 包体积 | 需要包含编译器(约 10KB gzip) | 不需要编译器 |
| 性能 | 首次渲染有编译开销 | 无运行时编译开销 |
| 使用场景 | CDN 引入、动态模板 | .vue 文件开发(主流) |
| Vue 包名 | vue.global.js(完整版) | vue.runtime.global.js(运行时版) |
8.2 自定义编译器扩展
Vue 3 编译器的插件化架构使得开发者可以自定义编译行为:
import { compile } from '@vue/compiler-dom'
const { code } = compile('<div>{{ msg }}</div>', {
// 自定义节点转换插件
nodeTransforms: [
(node, context) => {
// 在编译阶段拦截节点进行自定义处理
if (node.type === 1 && node.tag === 'my-component') {
// 自定义组件的编译逻辑
console.log('发现自定义组件:', node.tag)
}
}
],
// 自定义指令转换
directiveTransforms: {
// 自定义 v-focus 指令的编译行为
focus(dir, node, context) {
return {
props: [],
needRuntime: true // 需要运行时指令处理
}
}
}
})
8.3 编译器的错误处理
Vue 编译器在编译阶段就能捕获大量模板错误,提供友好的错误提示:
// 这些错误会在编译阶段被发现
// 1. 未闭合的标签
// <div><p></div> => "Element is missing end tag"
// 2. 无效的表达式
// {{ if (true) {} }} => "Invalid expression"
// 3. 重复的属性
// <div id="a" id="b"> => "Duplicate attribute"
// 4. v-if 和 v-for 同时使用
// <div v-if="show" v-for="i in list"> => 会给出优先级警告
// 5. 模板根节点校验(Vue 2)
// Vue 2 要求只能有一个根节点,Vue 3 支持多根节点(Fragment)
9. 编译器工作的可视化
你可以使用 Vue SFC Playground(https://play.vuejs.org)来实时查看 Vue 编译器的输出。在右侧面板选择 JS 或 AST 标签,即可看到模板编译的结果。
另外,Vue Template Explorer(https://template-explorer.vuejs.org)也是一个专门用于查看编译输出的工具,可以对比不同编译选项的效果。
10. 面试常见问题
Q1:Vue 的编译器做了什么?它分为哪几个阶段?
参考回答:
Vue 编译器负责将模板字符串编译为渲染函数(render function)。整个编译过程分为三个阶段:
-
Parse(解析):将模板字符串解析为 AST(抽象语法树)。解析器通过有限状态机逐字符扫描模板,识别标签、属性、文本、插值表达式、指令等语法结构,构建出树形的 AST。
-
Transform(转换):遍历 AST,对节点进行分析和改写。这个阶段使用访问者模式,通过一系列转换插件处理
v-if、v-for、v-model等指令节点,同时执行静态分析——标记 PatchFlag、收集动态节点、识别可提升的静态节点。 -
Generate(代码生成):将转换后的 AST 转换为 JavaScript 代码字符串,即最终的渲染函数。
Q2:什么是 PatchFlag?它是怎么优化 Diff 性能的?
参考回答:
PatchFlag 是 Vue 3 编译器在编译阶段为每个 VNode 生成的数字标记,用于指示该节点的哪些部分是动态的。常见的值包括 TEXT(1) 表示动态文本、CLASS(2) 表示动态 class、PROPS(8) 表示动态 props 等。
运行时 patch 算法会读取 PatchFlag,只对标记为动态的部分进行 Diff,跳过所有静态内容。例如一个节点的 PatchFlag 为 1(TEXT),运行时只需对比它的 textContent,而无需检查 props、class、style 等。
这是一种「编译时分析 + 运行时利用」的优化策略,将模板编译阶段能确定的静态/动态信息传递给运行时,极大减少了不必要的比较。
Q3:什么是静态提升(hoistStatic)?有什么好处?
参考回答:
静态提升是指编译器将完全静态的 VNode(没有任何动态绑定的节点)提升到渲染函数外部,作为模块级常量。这些 VNode 在组件的整个生命周期中只会被创建一次,后续重新渲染时直接复用引用。
好处有两个:
- 减少 VNode 创建开销:每次渲染不再重复创建静态 VNode 对象
- 减少 GC 压力:静态 VNode 不会被回收,减少了垃圾回收的频率
不仅元素节点可以被提升,纯静态的 props 对象也会被提升。
Q4:什么是 Block Tree?为什么需要它?
参考回答:
Block Tree 是 Vue 3 引入的一种优化机制。Block 是一种特殊的 VNode,它在创建时会收集其所有后代节点中的动态节点到一个扁平数组 dynamicChildren 中。
在运行时 patch 阶段,对于 Block 节点不需要递归遍历整棵子树,而是直接遍历 dynamicChildren 数组进行靶向更新,跳过所有静态节点。
需要注意的是,v-if 和 v-for 等结构性指令会开启新的 Block,因为它们可能改变子树的结构,打破父 Block 中 dynamicChildren 的对应关系。
Q5:Vue 3 的编译器相比 Vue 2 做了哪些优化?
参考回答:
Vue 3 的编译器相比 Vue 2 做了以下重大优化:
- PatchFlag(补丁标记):精确标记每个动态绑定类型,运行时只 Diff 动态部分。Vue 2 只能标记整棵静态子树。
- 静态提升(hoistStatic):将静态节点和属性提升到渲染函数外部,Vue 2 没有此优化。
- Block Tree:扁平化收集动态节点,跳过层级遍历。Vue 2 需要全量递归 Diff。
- 事件缓存(cacheHandlers):缓存内联事件处理函数,避免子组件不必要的更新。
- 预字符串化:大量连续静态节点合并为 HTML 字符串,通过 innerHTML 创建。
- Tree-shaking 支持:运行时 helper 按需引入,未使用的 API 不会进入最终产物。
这些优化使得 Vue 3 在更新性能上相比 Vue 2 有显著提升。
Q6:Vue 编译器中 Parse 阶段是如何工作的?
参考回答:
Parse 阶段使用有限状态机对模板字符串进行逐字符扫描。维护一个游标(source 字符串不断截取),根据当前字符决定进入哪个解析状态:
- 遇到
<进入标签解析(进一步判断是开始标签、结束标签还是注释) - 遇到
{{进入插值表达式解析 - 其他情况视为文本内容
元素解析会递归进行——解析完开始标签后递归调用 parseChildren 处理子节点,直到遇到对应的结束标签。同时维护一个祖先栈(ancestors)来追踪嵌套关系、校验标签是否正确闭合。
最终产出一棵完整的 AST,每个节点包含 type(类型)、tag(标签名)、props(属性/指令)、children(子节点)、loc(源码位置信息)等字段。
Q7:运行时编译和预编译有什么区别?应该用哪个?
参考回答:
- 运行时编译:在浏览器中将模板字符串编译为渲染函数。需要引入完整版 Vue(包含编译器,约多 10KB gzip)。适用于 CDN 直接引入或需要动态生成模板的场景。
- 预编译:在构建阶段(webpack / Vite)通过
vue-loader或vite-plugin-vue将.vue文件的模板编译为渲染函数。产物中不包含编译器代码,体积更小、首屏更快。
应该用预编译。现代 Vue 项目几乎都使用 .vue 单文件组件开发,构建工具会自动处理预编译。只有极少数场景(如后端下发动态模板)才需要运行时编译。
Q8:Vue 编译器为什么采用三阶段架构?不能一步到位吗?
参考回答:
三阶段架构(Parse → Transform → Generate)是编译器设计的经典模式,好处在于:
-
职责分离:每个阶段只关注一件事——Parse 只管把字符串变成 AST,Transform 只管分析和优化,Generate 只管拼代码。各阶段独立开发和测试。
-
可扩展性:Transform 阶段采用插件化设计,不同平台(DOM / SSR)可以注入不同的转换插件,共享 Parse 和 Generate 的逻辑。新增一个指令的编译支持只需添加一个转换插件。
-
多目标输出:同一份 AST 可以生成不同目标的代码(浏览器渲染函数 / SSR 字符串拼接函数),只需更换 Generate 实现。
-
优化的便利性:静态分析、PatchFlag 标记、静态提升等优化都在 Transform 阶段集中进行,不会影响 Parse 和 Generate 的实现。
如果一步到位(边解析边生成代码),代码会高度耦合,无法独立优化和扩展。
Q9:@vue/compiler-core 和 @vue/compiler-dom 有什么区别?为什么要分包?
参考回答:
@vue/compiler-core是与平台无关的编译器核心,包含 Parse、Transform、Generate 三大阶段的通用实现,不包含任何 DOM 相关的逻辑。@vue/compiler-dom基于 core 扩展了浏览器 DOM 平台的特定处理,如v-html、v-text、v-model(针对不同 input 类型生成不同代码)、v-show、v-on的修饰符(.prevent、.stop)等。
分包的原因是为了支持跨平台编译。Vue 3 不仅可以运行在浏览器中,还可以通过自定义渲染器运行在其他环境(如小程序、Canvas、终端等)。这些平台可以基于 compiler-core 实现自己的平台编译器,而不需要引入 DOM 相关的代码。
Q10:编译器如何处理 v-model 指令?
参考回答:
v-model 是一个语法糖,编译器在 Transform 阶段会将它展开为具体的 prop 和事件绑定。根据使用场景不同,展开的结果也不同:
<!-- 普通 input -->
<input v-model="msg" />
<!-- 编译为 -->
<input :value="msg" @input="msg = $event.target.value" />
<!-- 组件 -->
<MyComp v-model="msg" />
<!-- 编译为 -->
<MyComp :modelValue="msg" @update:modelValue="val => msg = val" />
<!-- 带参数的组件 v-model -->
<MyComp v-model:title="title" />
<!-- 编译为 -->
<MyComp :title="title" @update:title="val => title = val" />
编译器还会处理修饰符:.trim(添加 trim() 逻辑)、.number(添加 toNumber() 转换)、.lazy(将 input 事件改为 change 事件)。这些都在编译阶段就确定了,不需要运行时判断。
11. 总结
Vue 编译器是 Vue 框架中非常精巧的一个模块。它的核心价值在于将模板编译阶段能确定的信息尽可能多地传递给运行时,让运行时做最少的工作。这种「编译时优化、运行时受益」的设计思路,是 Vue 3 性能大幅提升的关键所在。
理解编译器的工作原理,不仅有助于写出更高性能的 Vue 代码(比如减少不必要的动态绑定),也能在遇到编译相关的 bug 时更快地定位问题。