文章目录
- 前言
- 一、编译流程概览
- 二、静态提升(HoistStatic)
-
- [2.1 原理](#2.1 原理)
- [2.2 静态属性也会提升](#2.2 静态属性也会提升)
- [2.3 收益](#2.3 收益)
- [三、PatchFlag 动态标记](#三、PatchFlag 动态标记)
-
- [3.1 原理](#3.1 原理)
- [3.2 常见 PatchFlag 类型](#3.2 常见 PatchFlag 类型)
- [3.3 对比 Vue 2](#3.3 对比 Vue 2)
- [四、Block Tree](#四、Block Tree)
-
- [4.1 问题](#4.1 问题)
- [4.2 Block 与 dynamicChildren](#4.2 Block 与 dynamicChildren)
- [4.3 Diff 时的行为](#4.3 Diff 时的行为)
- [4.4 与 PatchFlag 配合](#4.4 与 PatchFlag 配合)
- 五、缓存事件处理函数(cacheHandlers)
-
- [5.1 问题](#5.1 问题)
- [5.2 编译优化](#5.2 编译优化)
- [5.3 限制](#5.3 限制)
- 六、其他编译相关优化
-
- [6.1 静态字符串提升](#6.1 静态字符串提升)
- [6.2 v-once](#6.2 v-once)
- [6.3 v-memo(Vue 3.2+)](#6.3 v-memo(Vue 3.2+))
- [七、Vue 3 为什么更快(编译视角)](#七、Vue 3 为什么更快(编译视角))
- 八、面试聚焦
-
- [8.1 Vue 3 比 Vue 2 快的核心原因(编译层)](#8.1 Vue 3 比 Vue 2 快的核心原因(编译层))
- [8.2 PatchFlag 有哪些类型?](#8.2 PatchFlag 有哪些类型?)
- [8.3 静态提升能减少什么开销?](#8.3 静态提升能减少什么开销?)
- [8.4 Block Tree 是什么?](#8.4 Block Tree 是什么?)
- 九、易混淆点
- 十、思考与练习
- 总结
前言
Vue 3 的性能提升不仅来自 Proxy 响应式和快速 Diff,更来自编译时优化:在构建阶段分析模板,减少运行时 VNode 创建和 Diff 范围。本篇会讲清楚:
- 静态提升(HoistStatic)
- PatchFlag 动态标记
- Block Tree 与事件缓存
- Vue 3 比 Vue 2 快的编译层原因
一、编译流程概览
.vue 模板
↓ parse(解析为 AST)
↓ transform(静态分析 + 优化标记)
↓ generate(生成 render 函数)
render 函数执行 → VNode → patch → DOM
核心思想:能在编译期确定的(静态结构、动态位点),就不放到运行时反复计算。
| 优化手段 | 编译期做什么 | 运行时收益 |
|---|---|---|
| 静态提升 | 静态 VNode 提到函数外 | 不重复创建 |
| PatchFlag | 标记哪里会变 | Diff 只比动态位 |
| Block Tree | 收集 dynamicChildren | 跳过静态子树 |
| cacheHandlers | 缓存内联事件函数 | 减少子组件无效更新 |
二、静态提升(HoistStatic)
2.1 原理
模板中完全静态 的节点(无绑定、无指令),编译时提升到 render 函数外部,整个应用生命周期只创建一次 VNode,后续 render 直接复用。
vue
<template>
<div>
<h1>固定标题</h1>
<p>静态段落</p>
<p>{{ msg }}</p>
</div>
</template>
javascript
// 编译结果示意(简化)
const _hoisted_1 = createStaticVNode(`<h1>固定标题</h1>`, 1)
const _hoisted_2 = createStaticVNode(`<p>静态段落</p>`, 1)
function render(_ctx) {
return createElementVNode('div', null, [
_hoisted_1, // 复用,不重新创建
_hoisted_2,
createElementVNode('p', null, _ctx.msg, PatchFlags.TEXT)
])
}
2.2 静态属性也会提升
vue
<div class="container" id="main">
<span>hello</span>
</div>
整个静态子树可一次性提升,包括静态 class、id 等。
2.3 收益
- 避免每次 render 重新创建静态 VNode
- 静态子树不参与后续 Diff
- 大型模板中静态区域越多,收益越明显
三、PatchFlag 动态标记
3.1 原理
编译器分析模板,给含动态内容的 VNode 打上 patchFlag 位掩码,告诉运行时「只有这些部分可能变」。
vue
<div :class="cls">{{ msg }}</div>
javascript
createElementVNode(
'div',
{ class: _ctx.cls },
_ctx.msg,
PatchFlags.TEXT | PatchFlags.CLASS // 3
)
Diff 时只比较 TEXT 和 CLASS,不遍历整个 props 对象。
3.2 常见 PatchFlag 类型
| 值 | 名称 | 含义 |
|---|---|---|
| 1 | TEXT | 动态文本内容 |
| 2 | CLASS | 动态 class |
| 4 | STYLE | 动态 style |
| 8 | PROPS | 动态 props(非 class/style) |
| 16 | FULL_PROPS | props 绑定对象可能整体替换 |
| 32 | NEED_HYDRATION | SSR 水合需要 |
| 128 | KEYED_FRAGMENT | 带 key 的 Fragment |
| 256 | UNKEYED_FRAGMENT | 无 key 的 Fragment |
| 512 | NEED_PATCH | 子节点需 Diff |
| 1024 | DYNAMIC_SLOTS | 动态插槽 |
| -1 | HOISTED | 静态提升节点 |
| -2 | BAIL | 全量 Diff(优化失败回退) |
多个标记用按位或 组合,如 TEXT | CLASS = 3。
3.3 对比 Vue 2
Vue 2 运行时对 VNode 全量比较 props 和 children。Vue 3 有 patchFlag 时按位检查,跳过未标记部分,减少比较次数。
javascript
// Vue 3 运行时示意
if (patchFlag & PatchFlags.TEXT) {
// 只比文本
}
if (patchFlag & PatchFlags.CLASS) {
// 只比 class
}
// patchFlag === 0 → 该节点完全静态,可跳过
四、Block Tree
4.1 问题
传统 VNode 树是嵌套结构,Diff 需递归整棵树。模板里往往大量静态节点包裹少量动态节点,全树递归浪费。
4.2 Block 与 dynamicChildren
编译器为每个「Block 根」收集动态子节点 到扁平数组 dynamicChildren:
vue
<template>
<div> <!-- Block 根 -->
<header>静态导航</header>
<p>{{ title }}</p> <!-- 动态 → dynamicChildren[0] -->
<footer>静态页脚</footer>
<span>{{ count }}</span> <!-- 动态 → dynamicChildren[1] -->
</div>
</template>
javascript
// Block VNode 示意
{
type: 'div',
dynamicChildren: [
pVNode, // {{ title }}
spanVNode // {{ count }}
]
}
4.3 Diff 时的行为
更新时只遍历 dynamicChildren,静态的 header、footer 子树整段跳过,无需递归进入。
传统:递归 div → header → p → footer → span(5 层)
Block:直接 patch dynamicChildren [p, span](2 个节点)
4.4 与 PatchFlag 配合
- PatchFlag:单个节点内「哪些属性」动态
- Block Tree:整棵子树里「哪些节点」动态
两者叠加,大幅缩小 Diff 范围。
五、缓存事件处理函数(cacheHandlers)
5.1 问题
vue
<button @click="count++">+1</button>
每次 render 都会生成新的内联函数引用,子组件可能认为 props 变化而无效更新。
5.2 编译优化
编译器将稳定的事件处理器缓存到 render 函数上下文:
vue
<button @click="handleClick">点击</button>
javascript
// 编译结果示意
function render(_ctx, _cache) {
return createElementVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, '点击')
}
同一 render 周期内复用 _cache[0],避免每次新建函数,减少子组件 patch。
5.3 限制
依赖外部变量且每次需新闭包的内联箭头函数,可能无法缓存或标记 BAIL 回退全量 Diff。
六、其他编译相关优化
6.1 静态字符串提升
动态绑定中的静态字符串 也会提升,如 class="fixed" 与 :class="dynamic" 共存时,静态部分不参与重复创建。
6.2 v-once
vue
<header v-once>
<h1>{{ title }}</h1>
</header>
运行时指令:该子树只渲染一次,后续更新跳过 Diff。与编译静态提升不同,v-once 是开发者手动标记。
6.3 v-memo(Vue 3.2+)
vue
<div v-memo="[item.id, item.selected]">
<ExpensiveItem :item="item" />
</div>
依赖数组不变则跳过该子树更新,适合大列表项细粒度优化。
七、Vue 3 为什么更快(编译视角)
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 静态节点 | 每次 render 重建 VNode | 静态提升,只建一次 |
| Diff 范围 | 全树递归 | Block + dynamicChildren |
| 节点比较 | 全量 props/children | PatchFlag 按位比较 |
| 事件函数 | 每次新建 | cacheHandlers 缓存 |
| 列表 Diff | 双端 Diff | 快速 Diff + LIS |
编译优化 + 运行时 Diff 优化共同作用,使 Vue 3 在典型业务模板下更新更快、内存更省。
八、面试聚焦
8.1 Vue 3 比 Vue 2 快的核心原因(编译层)
静态提升、PatchFlag、Block Tree、事件缓存,减少 VNode 创建量和 Diff 范围;配合 Proxy 响应式和快速 Diff。
8.2 PatchFlag 有哪些类型?
TEXT(1)、CLASS(2)、STYLE(4)、PROPS(8) 等,按位或组合;HOISTED(-1) 表示静态提升节点。
8.3 静态提升能减少什么开销?
避免每次 render 重建静态 VNode,静态子树不参与 Diff,降低 GC 压力。
8.4 Block Tree 是什么?
编译期收集动态节点到 dynamicChildren,Diff 只比扁平动态列表,跳过静态子树。
九、易混淆点
- 编译优化仅针对模板:手写 render 函数默认享受不到,需自己优化。
- PatchFlag 与 Block 互补:一个管节点内动态位,一个管树里哪些节点动态。
- 静态提升 ≠ v-once:前者编译自动识别;后者手动声明跳过更新。
- BAIL(-2):优化无法应用时回退全量 Diff,保证正确性。
- 优化基于模板静态分析:过于动态的模板优化效果有限。
十、思考与练习
1. 静态提升做了什么?
解析:编译期把静态 VNode 提到 render 外,只创建一次,后续复用,不参与 Diff。
2. PatchFlag 的作用?
解析:位掩码标记 VNode 哪些部分动态,Diff 时只比较标记位,跳过静态 props/children。
3. Block Tree 如何减少 Diff?
解析:dynamicChildren 扁平收集动态节点,更新时只 patch 这些节点,静态子树整段跳过。
4. cacheHandlers 解决什么问题?
解析:缓存内联事件函数引用,避免每次 render 新函数导致子组件误判 props 变化。
5. 开发者如何配合编译优化?
解析:模板保持结构清晰、静态与动态分离;大列表用稳定 key;静态大块可用 v-once;昂贵子树用 v-memo。
总结
- 静态提升:静态 VNode 只创建一次,复用 + 跳过 Diff
- PatchFlag:按位标记 TEXT/CLASS/STYLE/PROPS 等动态内容
- Block Tree:dynamicChildren 扁平化,只 Diff 动态节点
- cacheHandlers:缓存事件函数,减少无效更新
- 本质:编译期分析模板,把运行时工作前移,是 Vue 3 性能的关键