本篇关键词:diff、patch、双端对比、最长递增子序列(LIS)、PatchFlags、Block Tree、靶向更新
难度等级:⭐⭐⭐⭐⭐
工程价值 :diff 是 Vue 渲染系统的"心脏手术台"。不懂 diff,你就无法解释"为什么我的列表更新这么慢",也无法写出真正高性能的 Vue 应用。本篇将彻底拆解 Vue3 diff 的数学之美。
一、开篇:Vue3 到底快在哪里?
很多人说 Vue3 快,是因为:
-
Proxy 比
Object.defineProperty快 -
编译优化
-
Tree Shaking
👉 但这些都不是最关键的原因。
真正的核心答案是:
Vue3 的 diff 算法,从"全量暴力对比"进化成了"靶向更新"。
-
Vue2:不管节点动没动,都要扫一遍
-
Vue3:只关心"可能会变的节点"
二、Vue2 的 diff:双端对比的遗憾
1️⃣ Vue2 diff 核心思想
Vue2 使用的是经典的 双端对比(Double-ended Diff):
旧:a b c d
新:a d b c
对比顺序:
-
旧头 ↔ 新头
-
旧尾 ↔ 新尾
-
旧头 ↔ 新尾
-
旧尾 ↔ 新头
2️⃣ Vue2 diff 的问题
// 旧 VNode
[a, b, c, d]
// 新 VNode
[d, a, b, c]
Vue2 的处理方式:
-
循环移动节点
-
多次 DOM 操作
-
时间复杂度 O(n)
问题在于:
即使大部分节点是静态的,也要参与 diff。
3️⃣ 致命短板:无法跳过静态节点
<div>
<span>静态1</span>
<span>静态2</span>
<span>{{ dynamic }}</span>
<span>静态3</span>
</div>
Vue2:
-
4 个 span 全部参与 diff
-
静态节点反复比较
👉 这是性能浪费的根源。
三、Vue3 的破局之道:Block Tree + PatchFlags
1️⃣ 编译期预知"谁是动态的"
Vue3 的编译器在编译模板时,已经知道:
<span>静态</span> <!-- ❌ 不会变 -->
<span :class="cls">动态</span> <!-- ✅ 会变 -->
编译器会给动态节点打上 PatchFlags:
{
patchFlag: PatchFlags.CLASS,
dynamicChildren: [spanVNode]
}
2️⃣ Block Tree:只存动态节点
blockVNode = {
dynamicChildren: [
vnode1, // class 动态
vnode2 // text 动态
]
}
👉 diff 的范围从"整棵树"缩小到"动态节点数组"。
四、Vue3 的 diff:靶向更新流程
1️⃣ patchBlockChildren(核心)
源码位置:packages/runtime-core/src/renderer.ts
function patchBlockChildren(
oldChildren,
newChildren,
fallbackContainer,
parentComponent
) {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// ✅ 只 patch 动态节点
patch(oldVNode, newVNode, fallbackContainer, parentComponent)
}
}
👉 静态节点在 diff 阶段完全消失。
2️⃣ 为什么这叫"靶向更新"?
医学比喻:
-
Vue2:全身 CT 扫描
-
Vue3:只检查病灶部位
工程比喻:
-
Vue2:全量 diff
-
Vue3:定向 patch
五、最长递增子序列(LIS):Vue3 的算法灵魂
1️⃣ 问题场景:列表移动的最优解
// 旧列表
[a, b, c, d]
// 新列表
[a, d, b, c]
目标:
-
最少次数的 DOM 移动
-
尽可能复用节点
2️⃣ 暴力解法的代价
如果逐个对比并移动:
-
最坏情况:O(n²)
-
DOM 操作昂贵
3️⃣ LIS 的数学直觉
最长递增子序列:
在原序列中,找出一个最长的子序列,使其元素顺序不变。
示例:
旧索引:[0, 1, 2, 3]
新顺序:[0, 3, 1, 2]
LIS = [0, 1, 2]
👉 LIS 中的节点不动,其余节点移动。
4️⃣ Vue3 中的 LIS 实现(简化版)
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i = 0, j, u, v
for (i = 0; i < arr.length; i++) {
const val = arr[i]
if (val !== 0) {
j = result[result.length - 1]
if (arr[j] < val) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
const mid = (u + v) >> 1
if (arr[result[mid]] < val) {
u = mid + 1
} else {
v = mid
}
}
if (val < arr[result[u]]) {
if (u > 0) p[i] = result[u - 1]
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
✅ **时间复杂度:O(n log n)**
✅ DOM 移动次数最少
六、key 的真正作用:节点复用与 LIS
1️⃣ 没有 key 的情况
<div v-for="item in list">{{ item }}</div>
Vue 会:
-
就地更新
-
无法识别节点身份
-
LIS 失效
2️⃣ 有 key 的情况
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
Vue 可以:
-
精确匹配新旧节点
-
构建索引映射
-
正确计算 LIS
👉 key 是 LIS 算法的输入前提。
七、Vue2 vs Vue3 diff:全方位对比
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 对比范围 | 全树 | 仅动态节点 |
| 算法 | 双端对比 | LIS + 靶向更新 |
| 静态节点 | 参与 diff | 完全跳过 |
| key 的重要性 | 高 | 极高 |
| 时间复杂度 | O(n) | O(n log n)(最优移动) |
| 编译优化 | 无 | PatchFlags + Block Tree |
八、调试实战:观察 diff 的"靶向性"
1️⃣ 打这些断点
packages/runtime-core/src/renderer.ts
├── patchBlockChildren()
├── move()
├── getSequence()
└── patchChildren()
2️⃣ 测试模板
<div>
<span>静态1</span>
<span :class="cls">动态</span>
<span>静态2</span>
</div>
观察:
-
patchBlockChildren的newChildren长度 -
静态节点是否出现
-
move()的调用次数
👉 你会发现:静态节点从未进入 diff。
九、工程最佳实践(VIP 总结)
✅ 永远给 v-for 加 key
<div v-for="item in list" :key="item.id">
✅ 减少动态节点数量
<!-- ❌ 动态根节点 -->
<div :class="wrapperCls">
<span>内容</span>
</div>
<!-- ✅ 静态根节点 -->
<div class="wrapper">
<span :class="contentCls">内容</span>
</div>
✅ 合理使用 v-once
<div v-once>{{ expensiveCalc }}</div>
✅ 避免在循环中创建组件
<!-- ❌ -->
<div v-for="item in list">
<HeavyComponent />
</div>
<!-- ✅ -->
<HeavyComponent v-for="item in list" :key="item.id" />
十、面试追问
-
为什么 Vue3 还要 diff?
因为需要确定 DOM 的移动、复用和更新策略。
-
PatchFlags 和 diff 的关系?
PatchFlags 决定"是否需要 diff",diff 决定"如何更新"。
-
LIS 算法为什么能减少 DOM 操作?
因为它找到了"最大不动集",只移动必要节点。
-
Vue3 的 diff 一定比 Vue2 快吗?
在动态节点多的场景下明显更快;纯静态场景下差异不大。
十一、本篇总结
✅ Vue2 diff 是全量双端对比,静态节点无法跳过
✅ Vue3 通过 PatchFlags + Block Tree 实现靶向更新
✅ 最长递增子序列(LIS)算法最小化 DOM 移动
✅ key 是节点复用和 LIS 的前提
✅ 编译期优化是运行时性能的基础
一句话升华:
Vue3 的 diff 不是"更快的比对",而是**"更少的比对"**。性能的提升,来自于"不做不必要的事"。