Vue 3 编译器原理深入
模板编译流程、静态提升、Patch Flag、Block Tree 优化详解
什么是 Vue 编译器?
定义:将模板字符串转换为渲染函数的编译系统。
核心作用:
- 模板解析:将 HTML 模板转为 AST(抽象语法树)
- 优化转换:静态分析和编译时优化
- 代码生成:生成高效的渲染函数代码
编译时机:
- 构建时编译:使用 Vite/Webpack 插件(推荐)
- 运行时编译:使用
vue/dist/vue.esm-bundler.js(体积更大)
一、编译流程概览
1.1 三个核心阶段
typescript
function compile(template: string, options?: CompilerOptions) {
// 1. Parse - 解析模板为 AST
const ast = parse(template, options)
// 2. Transform - 转换和优化 AST
transform(ast, options)
// 3. Generate - 生成渲染函数代码
const code = generate(ast, options)
return code
}流程图:
模板字符串
↓
【Parse 阶段】
↓
AST 树
↓
【Transform 阶段】
↓
优化后的 AST
↓
【Generate 阶段】
↓
渲染函数代码1.2 示例演示
输入模板:
vue
<template>
<div id="app">
<h1>{{ title }}</h1>
<p>Static text</p>
<button @click="increment">{{ count }}</button>
</div>
</template>输出渲染函数:
javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "Static text", -1)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_hoisted_2,
_createElementVNode("button", {
onClick: _ctx.increment
}, _toDisplayString(_ctx.count), 9 /* TEXT, PROPS */)
]))
}二、Parse 阶段 - 模板解析
2.1 词法分析(Tokenization)
将模板字符串分解为 tokens:
typescript
enum TokenType {
Text, // 文本
Tag, // 标签
Attribute, // 属性
Interpolation, // 插值 {{ }}
Comment, // 注释
CDATA
}
interface Token {
type: TokenType
content: string
loc: SourceLocation
}
// 示例
const template = '<div id="app">{{ msg }}</div>'
// Tokens:
[
{ type: TokenType.Tag, content: '<div' },
{ type: TokenType.Attribute, content: 'id="app"' },
{ type: TokenType.Text, content: '>' },
{ type: TokenType.Interpolation, content: '{{ msg }}' },
{ type: TokenType.Tag, content: '</div>' }
]2.2 语法分析(Parsing)
将 tokens 构建为 AST:
typescript
interface ElementNode {
type: NodeTypes.ELEMENT
tag: string
props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[]
isSelfClosing: boolean
codegenNode?: VNodeCall
}
interface TextNode {
type: NodeTypes.TEXT
content: string
}
interface InterpolationNode {
type: NodeTypes.INTERPOLATION
content: ExpressionNode
}
// AST 示例
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [
{ type: NodeTypes.ATTRIBUTE, name: 'id', value: 'app' }
],
children: [
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'msg'
}
}
]
}2.3 解析器实现(简化版)
typescript
function parse(template: string): RootNode {
const context = createParserContext(template)
const children = parseChildren(context, [])
return {
type: NodeTypes.ROOT,
children,
loc: getSelection(context, 0)
}
}
function parseChildren(
context: ParserContext,
ancestors: ElementNode[]
): TemplateChildNode[] {
const nodes: TemplateChildNode[] = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node: TemplateChildNode | undefined
if (s.startsWith('{{')) {
// 解析插值
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (s[1] === '/') {
// 结束标签
parseTag(context, TagType.End)
continue
} else if (/[a-z]/i.test(s[1])) {
// 元素节点
node = parseElement(context, ancestors)
}
}
if (!node) {
// 文本节点
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode {
// 解析开始标签
const element = parseTag(context, TagType.Start)
// 自闭合标签
if (element.isSelfClosing) {
return element
}
// 递归解析子节点
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
// 解析结束标签
parseTag(context, TagType.End)
return element
}三、Transform 阶段 - AST 转换优化
3.1 转换流程
typescript
function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
// 遍历 AST,应用转换插件
traverseNode(root, context)
// 创建根代码生成节点
createRootCodegen(root, context)
// 完成转换
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
}
function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
// 应用节点转换
const { nodeTransforms } = context
const exitFns: Array<() => void> = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
exitFns.push(onExit)
}
}
// 递归处理子节点
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
break
}
// 执行退出函数(后序遍历)
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}3.2 核心转换插件
transformElement - 元素转换:
typescript
export const transformElement: NodeTransform = (node, context) => {
return function postTransformElement() {
if (node.type !== NodeTypes.ELEMENT) return
const { tag, props } = node
const isComponent = !isHTMLTag(tag)
// 构建 VNode 调用
const vnodeTag = isComponent ? resolveComponentType(node, context) : `"${tag}"`
const vnodeProps = buildProps(node, context)
const vnodeChildren = node.children
// 创建 VNode 调用节点
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
getPatchFlag(node),
getDynamicPropNames(node)
)
}
}transformText - 文本合并:
typescript
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT
) {
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child)) {
// 合并相邻的文本节点
for (let j = i + 1; j < children.length; j++) {
const next = children[j]
if (isText(next)) {
if (!currentContainer) {
currentContainer = children[i] = {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [child]
}
}
currentContainer.children.push(' + ', next)
children.splice(j, 1)
j--
} else {
currentContainer = undefined
break
}
}
}
}
}
}
}四、静态提升(Static Hoisting)
4.1 什么是静态提升?
将静态节点提升到渲染函数外部,避免每次渲染都重新创建。
优化前:
javascript
export function render(_ctx) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("p", null, "Static text"),
_createElementVNode("p", null, "Another static")
]))
}优化后:
javascript
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "Static text", -1)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "Another static", -1)
export function render(_ctx) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_hoisted_2
]))
}4.2 静态提升的条件
可以提升:
- 纯静态文本节点
- 静态属性的元素节点
- 静态子树(所有子节点都是静态的)
不能提升:
- 包含动态绑定(
:class、v-bind) - 包含插值
- 包含指令(
v-if、v-for) - 引用了作用域变量
4.3 实现原理
typescript
function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root,
context,
new Map(),
isSingleElementRoot(root, root.children[0])
)
}
function walk(
node: ParentNode,
context: TransformContext,
resultCache: Map<TemplateChildNode, boolean>,
doNotHoistNode: boolean = false
) {
let hasHoistedNode = false
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
// 只处理元素和文本
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
// 判断是否可以静态提升
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
// 标记为可提升
;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED
child.codegenNode = context.hoist(child.codegenNode!)
hasHoistedNode = true
continue
}
}
// 递归处理子节点
if (child.children.length > 0) {
walk(child, context, resultCache)
}
}
}
return hasHoistedNode
}五、Patch Flag 优化
5.1 什么是 Patch Flag?
定义:编译时标记,指示节点的动态部分,用于运行时快速 Diff。
枚举定义:
typescript
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性(除 class/style)
FULL_PROPS = 1 << 4, // 有动态 key 的属性
HYDRATE_EVENTS = 1 << 5, // 事件监听器
STABLE_FRAGMENT = 1 << 6, // 稳定的 fragment
KEYED_FRAGMENT = 1 << 7, // 有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 fragment
NEED_PATCH = 1 << 9, // 需要 patch
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 静态提升
BAIL = -2 // 退出优化
}5.2 Patch Flag 示例
vue
<template>
<!-- 1. 动态文本 -->
<p>{{ msg }}</p>
<!-- PatchFlag: 1 (TEXT) -->
<!-- 2. 动态 class -->
<div :class="className"></div>
<!-- PatchFlag: 2 (CLASS) -->
<!-- 3. 动态 style -->
<div :style="styleObject"></div>
<!-- PatchFlag: 4 (STYLE) -->
<!-- 4. 动态属性 -->
<div :id="dynamicId"></div>
<!-- PatchFlag: 8 (PROPS), dynamicProps: ["id"] -->
<!-- 5. 事件监听 -->
<button @click="handleClick">Click</button>
<!-- PatchFlag: 32 (HYDRATE_EVENTS) -->
<!-- 6. 多个动态属性 -->
<div :id="id" :class="cls">{{ text }}</div>
<!-- PatchFlag: 9 (TEXT | PROPS), dynamicProps: ["id"] -->
</template>生成的代码:
javascript
// 动态文本
_createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
// 动态 class
_createElementVNode("div", {
class: _ctx.className
}, null, 2 /* CLASS */)
// 动态属性
_createElementVNode("div", {
id: _ctx.dynamicId
}, null, 8 /* PROPS */, ["id"])5.3 运行时优化
typescript
function patchElement(
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) {
const el = (n2.el = n1.el!)
const { patchFlag, dynamicChildren, dirs } = n2
// 有 patchFlag,进行靶向更新
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// 完整 props 对比
patchProps(el, n2, oldProps, newProps, parentComponent, isSVG)
} else {
// 靶向更新
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
if (patchFlag & PatchFlags.PROPS) {
// 只更新动态属性
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
hostPatchProp(el, key, oldProps[key], newProps[key], isSVG)
}
}
}
if (patchFlag & PatchFlags.TEXT) {
// 只更新文本内容
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
}
// 更新子节点
if (dynamicChildren) {
// 只更新动态子节点
patchBlockChildren(n1.dynamicChildren!, dynamicChildren, el, ...)
} else if (!optimized) {
// 完整 diff
patchChildren(n1, n2, el, ...)
}
}六、Block Tree 优化
6.1 什么是 Block?
定义:收集所有动态后代节点的特殊 VNode,用于跳过静态节点的 Diff。
传统 VNode 树:
div
├── p (static)
├── span (dynamic)
└── div
├── p (static)
└── span (dynamic)Block Tree:
Block (div)
dynamicChildren: [span, span] // 只收集动态节点6.2 Block 的创建
typescript
// 编译后的代码
export function render(_ctx) {
return (
_openBlock(), // 开启 Block 收集
_createElementBlock("div", null, [
_createElementVNode("p", null, "Static"),
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
_createElementVNode("div", null, [
_createElementVNode("p", null, "Static"),
_createElementVNode("span", null, _toDisplayString(_ctx.count), 1 /* TEXT */)
])
])
)
}运行时实现:
typescript
let currentBlock: VNode[] | null = null
const blockStack: VNode[][] = []
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
export function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
export function createElementBlock(
type: string | Component,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
return setupBlock(
createBaseVNode(type, props, children, patchFlag, dynamicProps)
)
}
function setupBlock(vnode: VNode) {
// 将收集到的动态子节点附加到 vnode
vnode.dynamicChildren = currentBlock || EMPTY_ARR
closeBlock()
// 当前 vnode 也可能是动态的,添加到父 Block
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
// 创建 VNode 时自动收集
export function createVNode(
type: VNodeTypes,
props?: (Data & VNodeProps) | null,
children?: unknown,
patchFlag?: number
): VNode {
const vnode = createBaseVNode(type, props, children, patchFlag)
// 如果有 patchFlag,添加到当前 Block
if (patchFlag > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}6.3 Block Tree 的 Diff 优化
typescript
function patchBlockChildren(
oldChildren: VNode[],
newChildren: VNode[],
fallbackContainer: RendererElement,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean
) {
// 只对比动态子节点,跳过静态节点
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 确定容器
const container =
oldVNode.el &&
(oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode))
? hostParentNode(oldVNode.el)!
: fallbackContainer
// 直接 patch,不需要遍历整棵树
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
true // optimized
)
}
}6.4 性能对比
传统 Diff:
- 需要遍历整棵树
- 对比所有节点(包括静态节点)
- 时间复杂度:O(n)
Block Tree Diff:
- 只遍历动态节点
- 跳过静态节点
- 时间复杂度:O(动态节点数)
示例:
vue
<template>
<div>
<p>Static 1</p>
<p>Static 2</p>
<p>Static 3</p>
<span>{{ msg }}</span> <!-- 唯一的动态节点 -->
<p>Static 4</p>
<p>Static 5</p>
</div>
</template>- 传统 Diff:需要对比 7 个节点
- Block Tree:只对比 1 个动态节点(span)
七、Generate 阶段 - 代码生成
7.1 代码生成流程
typescript
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const { push, indent, deindent, newline } = context
// 生成函数前导码
genFunctionPreamble(ast, context)
// 生成渲染函数
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(`return null`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: '',
map: context.map ? context.map.toJSON() : undefined
}
}7.2 节点代码生成
typescript
function genNode(node: CodegenNode, context: CodegenContext) {
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
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.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
}
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper } = context
const { tag, props, children, patchFlag, dynamicProps } = node
// 生成 createVNode 调用
push(helper(CREATE_ELEMENT_VNODE) + `(`)
// 生成参数
const args = [tag, props, children]
if (patchFlag) {
args.push(patchFlag + '')
}
if (dynamicProps) {
args.push(dynamicProps)
}
genNodeList(args, context)
push(`)`)
}7.3 完整示例
输入模板:
vue
<div id="app" :class="className">
<p>Static</p>
<span>{{ msg }}</span>
</div>生成的代码:
javascript
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
normalizeClass as _normalizeClass,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "Static", -1)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
id: "app",
class: _normalizeClass(_ctx.className)
}, [
_hoisted_2,
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
], 2 /* CLASS */))
}八、编译优化总结
8.1 优化对比
| 优化技术 | 作用 | 性能提升 |
|---|---|---|
| 静态提升 | 避免重复创建静态节点 | 减少内存分配 |
| Patch Flag | 靶向更新动态内容 | 减少 Diff 时间 |
| Block Tree | 跳过静态节点 Diff | 大幅减少遍历 |
| 事件缓存 | 缓存事件处理函数 | 避免重复创建 |
8.2 Vue 2 vs Vue 3 编译对比
Vue 2:
- 无编译时优化
- 运行时全量 Diff
- 无静态标记
Vue 3:
- 静态提升
- Patch Flag 靶向更新
- Block Tree 跳过静态节点
- 性能提升 1.3-2 倍
面试高频题
1: Vue 3 编译器的三个阶段是什么?
答案:
- Parse(解析):将模板字符串解析为 AST
- Transform(转换):遍历 AST,应用优化转换
- Generate(生成):将优化后的 AST 生成渲染函数代码
2: 什么是静态提升?有什么好处?
答案:
定义:将静态节点提升到渲染函数外部,避免每次渲染都重新创建。
好处:
- 减少内存分配
- 减少 GC 压力
- 提升渲染性能
示例:
javascript
// 提升前
function render() {
return h('div', [
h('p', 'Static') // 每次都创建
])
}
// 提升后
const _hoisted = h('p', 'Static') // 只创建一次
function render() {
return h('div', [_hoisted])
}3: Patch Flag 是什么?如何工作?
答案:
定义:编译时标记,指示节点的动态部分。
工作原理:
- 编译时分析节点,标记动态内容类型
- 运行时根据 flag 进行靶向更新,跳过静态部分
常见 Flag:
1:TEXT(动态文本)2:CLASS(动态 class)4:STYLE(动态 style)8:PROPS(动态属性)
4: Block Tree 是什么?解决了什么问题?
答案:
定义:收集所有动态后代节点的特殊 VNode。
解决的问题:
- 传统 Diff 需要遍历整棵树
- 大量静态节点浪费性能
优化效果:
- 只对比动态节点
- 跳过静态节点
- 性能提升显著(特别是静态内容多的场景)
5: Vue 3 编译器如何优化 v-for?
答案:
- Fragment 优化:使用 Fragment 避免额外包裹元素
- Patch Flag:标记为
KEYED_FRAGMENT或UNKEYED_FRAGMENT - Block 收集:每个列表项是一个 Block
- 稳定性检测:检测列表是否稳定(顺序不变)
javascript
// 有 key
_createElementBlock(Fragment, null, [
(_openBlock(true), _createElementBlock(Fragment, null,
_renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("div", { key: item.id }))
}),
128 /* KEYED_FRAGMENT */
))
])6: 编译时优化和运行时优化的区别?
答案:
编译时优化(Vue 3 重点):
- 静态提升
- Patch Flag 标记
- Block Tree 收集
- 发生在构建阶段
- 无运行时开销
运行时优化:
- 虚拟 DOM Diff
- 组件缓存(keep-alive)
- 异步组件
- 发生在浏览器中
- 有运行时开销
7: 什么情况下节点不能被静态提升?
答案:
不能提升的情况:
- 包含动态绑定(
:class、v-bind) - 包含插值
- 包含指令(
v-if、v-for、v-show) - 引用了组件状态或 props
- 使用了
ref或v-once
8: Vue 3 如何优化事件监听器?
答案:
事件缓存:
javascript
// 优化前
function render(_ctx) {
return h('button', {
onClick: () => _ctx.count++ // 每次都创建新函数
})
}
// 优化后
function render(_ctx, _cache) {
return h('button', {
onClick: _cache[0] || (_cache[0] = $event => _ctx.count++)
})
}好处:
- 避免重复创建函数
- 减少内存分配
- 避免子组件不必要的更新
9: 如何查看编译后的代码?
答案:
方法 1:Vue SFC Playground
- https://play.vuejs.org/
- 在线查看编译结果
方法 2:使用 @vue/compiler-sfc
javascript
import { compile } from '@vue/compiler-dom'
const { code } = compile(`
<div>{{ msg }}</div>
`, {
mode: 'module',
hoistStatic: true
})
console.log(code)方法 3:Vite 插件
javascript
// vite.config.js
export default {
plugins: [
vue({
template: {
compilerOptions: {
// 查看编译选项
}
}
})
]
}10: Vue 3 编译器相比 Vue 2 有哪些改进?
答案:
Vue 3 改进:
- 静态提升:Vue 2 无此优化
- Patch Flag:靶向更新,Vue 2 全量对比
- Block Tree:跳过静态节点,Vue 2 全树遍历
- 事件缓存:避免重复创建函数
- 更好的 Tree-shaking:按需引入辅助函数
- 更快的编译速度:优化的解析器
性能提升:
- 初始渲染:快 55%
- 更新性能:快 133%
- 内存使用:减少 54%