Vue3运行时性能远超Vue2的核心原因之一,是模板编译阶段引入的 Patch Flags(补丁标记) 和 Block Tree(块树) 优化。这两项技术从 "减少diff范围"和"精准定位更新"两个维度,彻底解决了Vue2中虚拟DOM全量diff的性能瓶颈。
一、Patch Flags:给动态节点 "贴标签",避免无意义 diff
1. 核心设计:标记动态节点的 "更新维度"
Vue 2 的虚拟 DOM diff 会遍历所有节点(无论静态 / 动态),对比每一个属性(如 class
/style
/text
),即使节点只有文本内容变化,也会冗余对比其他无关属性。
Vue 3 的 Patch Flags 则在编译阶段 ,为每个动态节点打上 "更新维度标记",明确该节点仅会在哪些场景下变化。运行时 diff 时,只需根据标记检查对应的属性,跳过无关对比。
(1)常用 Patch Flags 类型(源码定义)
标记常量 | 含义说明 | 对应场景示例 |
---|---|---|
TEXT (1) |
节点文本内容动态 | <div>{{ msg }}</div> |
CLASS (2) |
class 属性动态 |
<div :class="activeCls"></div> |
STYLE (4) |
style 属性动态 |
<div :style="{ color: red }"></div> |
PROPS (8) |
普通 props 动态(如 id /title ) |
<div :id="boxId"></div> |
FULL_PROPS (16) |
props 含动态键(如 :key="propKey" ) |
<div :[propKey]="value"></div> |
HYDRATE_EVENTS (32) |
节点绑定事件(仅 hydration 阶段用) | <button @click="handleClick"></button> |
STABLE_FRAGMENT (64) |
稳定片段(子节点顺序不变) | <template v-for="item in list" :key="item.id"> |
UNKEYED_FRAGMENT (128) |
无 key 的片段(需全量 diff 子节点) | <template v-for="item in list"> |
NEED_PATCH (256) |
节点需执行 patch 逻辑(如组件节点) | <MyComponent :msg="msg" /> |
2.如何减少 diff 计算量?
以一个简单模板为例,看编译前后的差异:
模板代码
vue
<template>
<div class="static-cls" :style="dynamicStyle">
<p>{{ dynamicText }}</p>
<button @click="handleClick">点击</button>
</div>
</template>
编译后生成的虚拟 DOM 节点(简化)
vue
// 根节点:div
const vnode = {
type: 'div',
props: {
class: 'static-cls', // 静态属性
style: dynamicStyle // 动态属性
},
patchFlag: 4, // STYLE 标记:仅 style 动态
children: [
// p 节点:TEXT 标记
{ type: 'p', children: dynamicText, patchFlag: 1 },
// button 节点:HYDRATE_EVENTS 标记(事件仅 hydration 用,运行时无需 diff)
{ type: 'button', props: { onClick: handleClick }, patchFlag: 32, children: '点击' }
]
}
运行时 diff 优化逻辑
当 dynamicText
变化时:
-
遍历虚拟 DOM 树时,跳过所有无 patchFlag 的静态节点 (如根节点的
class="static-cls"
是静态属性,直接跳过对比); -
对于有 patchFlag 的节点,仅对比标记对应的维度:
p
节点的 patchFlag 是TEXT
,仅对比文本内容,跳过class
/style
等无关属性;- 根节点的 patchFlag 是
STYLE
,但本次变化的是TEXT
,因此根节点无需 diff; button
节点的 patchFlag 是HYDRATE_EVENTS
,运行时无事件变化,直接跳过。
最终,diff 仅聚焦于 p
节点的文本内容,计算量比 Vue 2 减少 80% 以上。
二、Block Tree:按 "更新关联性" 分组,缩小 diff 范围
1. 核心设计:将动态节点 "聚类" 为 Block
Vue 2 的 diff 会从根节点开始全量遍历整个虚拟 DOM 树,即使页面大部分是静态内容(如头部导航、底部版权),也会被强制遍历。
Vue 3 的 Block Tree 则在编译阶段,将模板按 "动态节点关联性" 拆分为多个 "Block(块)":
- 一个 Block 是一组 "可能同时更新" 的动态节点集合(静态节点会被提升到 Block 外部,仅编译一次);
- 每个 Block 会记录其内部所有动态节点的引用,运行时仅需 diff Block 内的动态节点,无需遍历整个树。
关键特性:
- 静态提升(Static Hoisting) :Block 内的静态节点(如文本、无动态属性的标签)会被提取到 Block 外部,避免每次渲染都重新创建;
- 动态节点追踪 :Block 维护一个
dynamicChildren
数组,直接存储所有动态节点的引用,diff 时无需遍历子树,直接访问数组即可。
2. 如何减少 diff 计算量?
以一个包含列表和静态内容的模板为例:
模板代码
vue
<template>
<!-- 静态头部:导航栏 -->
<nav class="header">首页 | 列表 | 我的</nav>
<!-- 动态列表:可能更新 -->
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<!-- 静态底部:版权信息 -->
<footer class="footer">© 2025 Vue 3</footer>
</template>
编译后生成的 Block Tree(简化)
javascript
// 根 Block
const rootBlock = {
// 静态节点:头部和底部(仅创建一次,后续复用)
staticChildren: [
{ type: 'nav', class: 'header', children: '首页 | 列表 | 我的' },
{ type: 'footer', class: 'footer', children: '© 2025 Vue 3' }
],
// 动态节点集合:仅包含列表项(li)
dynamicChildren: [
// 列表项节点(每个 li 带 TEXT 标记)
{ type: 'li', patchFlag: 1, children: item1.name, key: item1.id },
{ type: 'li', patchFlag: 1, children: item2.name, key: item2.id }
]
}
运行时 diff 优化逻辑
当 list
数据变化时:
- 运行时直接定位到 根 Block 的
dynamicChildren
数组 ,无需遍历nav
和footer
等静态节点; - 仅对
dynamicChildren
中的列表项进行 diff(结合每个 li 的TEXT
标记,仅对比文本); - 静态节点(头部、底部)完全跳过 diff,直接复用之前的 DOM。
这种设计将 diff 范围从 "整个树" 缩小到 "动态节点数组",在静态内容占比高的页面(如官网、文档)中,性能提升尤为显著。
三、Patch Flags 与 Block Tree 的协同工作流程
两者并非独立优化,而是深度协同,形成 "精准定位 → 高效对比" 的完整链路:
-
编译阶段:
- 分析模板,将静态节点提升,动态节点按关联性分组为 Block;
- 为每个动态节点打上 Patch Flags,标记其更新维度;
- 每个 Block 维护
dynamicChildren
数组,存储内部所有带 Patch Flags 的节点。
-
运行时更新阶段:
- 数据变化触发组件更新,生成新的虚拟 DOM Block;
- 对比新旧 Block 的
dynamicChildren
数组(跳过静态节点); - 对数组中的每个动态节点,根据 Patch Flags 仅对比对应的属性(如 TEXT 仅比文本,STYLE 仅比样式);
- 生成最小化的 DOM 操作,执行更新。
四、优化失效的场景
虽然 Patch Flags 和 Block Tree 大幅提升了性能,但在某些场景下,这些优化会 "失效",导致 diff 计算量回升:
1. 无 key 的列表(v-for 缺少 key)
-
问题 :无 key 的列表会被标记为
UNKEYED_FRAGMENT
(128),此时 Block 无法通过 key 复用节点,只能全量 diff 子节点; -
原因:Vue 无法确定列表项的唯一标识,无法判断节点是否可复用,只能逐个对比;
vue<!-- 优化失效:无 key 的 v-for --> <ul> <li v-for="item in list">{{ item.name }}</li> <!-- patchFlag: 128 + 1 --> </ul>
2. 动态节点包含 "动态键" 的 props(FULL_PROPS 标记)
-
问题 :若 props 包含动态键(如
:[dynamicKey]="value"
),节点会被标记为FULL_PROPS
(16),此时需对比所有 props(无法跳过); -
原因 :动态键的取值不确定(如
dynamicKey
可能是id
/class
/title
),Vue 无法提前知道哪些 props 会变化,只能全量对比;vue<!-- 优化失效:动态键 props --> <div :[dynamicKey]="value"></div> <!-- patchFlag: 16 -->
3. 手动操作 DOM 或使用非响应式数据
-
问题 :若通过原生 API(如
document.getElementById
)手动修改 DOM,或使用markRaw
标记的非响应式数据,Vue 无法追踪变化,优化机制无法触发; -
原因:Patch Flags 和 Block Tree 依赖响应式系统触发更新,手动操作脱离了响应式追踪;
vueconst nonReactive = markRaw({ msg: 'hello' }) // 非响应式数据 // 修改 nonReactive.msg 不会触发更新,优化机制失效
4. 组件内部使用 setup
外的非响应式变量
-
问题 :若组件模板使用
setup
函数外定义的普通变量(非ref
/reactive
),变量变化不会触发更新,优化机制无意义; -
原因:非响应式变量无法触发依赖收集,即使模板有动态节点,也不会进入 diff 流程;
javascript// setup 外的普通变量(非响应式) let count = 0 setup() { return { count } // 模板使用 count,但变化不会触发更新 }
五、总结
Patch Flags 和 Block Tree 是 Vue 3 编译时优化的核心,其本质是 "编译阶段提前分析动态节点特征,运行时精准跳过无关计算":
- Patch Flags 解决 "diff 什么" 的问题:仅对比动态节点的指定属性;
- Block Tree 解决 "在哪里 diff" 的问题:仅 diff 动态节点集合,跳过静态内容。