核心目标:最小化 DOM 操作
DOM 操作(创建、插入、删除节点)是浏览器中 最昂贵的操作之一 。Diff 算法的全部意义在于:通过 JavaScript 层面的计算(CPU),找出最小的差异,从而减少对真实 DOM(浏览器渲染引擎)的调用。
vue2
Vue2 的 Diff 算法核心是 同级比较与双端比较,它的完整流程可概括为以下步骤:
算法核心:通过头头、尾尾、头尾、尾头四种直接比较,优先处理位置未变或简单移动的节点。只有这四种情况都不匹配时,才通过 key 映射表查找可复用节点,从而在 O(n) 时间复杂度内,以最少的 DOM 操作完成列表更新。
为什么是“双端”比较?(四个指针的策略)
这主要为了高效处理实际开发中最常见的数据变化场景,并为每种场景设计了最快捷的匹配路径:
-
场景一:在列表头部/尾部添加或删除项(最常见)
-
对应策略:
头头比较和尾尾比较 -
为什么高效:例如在列表头部新增一项。
旧列表[A,B,C]->新列表[D,A,B,C]。- 第一轮“头头比较”:旧A ≠ 新D,不匹配。
- 第一轮“尾尾比较”:旧C ≠ 新C?等一下,先比较尾部:旧C 对 新C?这里新列表的尾部是 C,旧列表尾部也是 C,匹配上了!算法会直接复用C节点并更新。
- 然后指针移动,继续“尾尾比较”:旧B 对 新B,再次匹配... 如此往复。
- 最终发现只有新节点 D 是新增的,只需一次
insertBefore操作。
-
总结:
头头/尾尾比较能瞬间处理掉列表前后顺序保持一致的连续区间,快速缩小需要对比的“混乱”中间区域。
-
-
场景二:列表反转或节点位置移动
-
对应策略:
头尾比较和尾头比较 -
为什么高效:例如反转列表。
旧列表[A,B,C,D]->新列表[D,C,B,A]。头头比较:A ≠ D。尾尾比较:D ≠ A。头尾比较:旧头A 对 新尾A?匹配! 这意味着旧列表的头节点,应该移动到新列表的尾部。一次insertBefore操作就能将 A 移动到最后。- 同理,下一轮
尾头比较会发现旧尾D 匹配新头D,将D移动到最前。
-
总结:
头尾/尾头比较是处理节点位置交叉移动的快速通道。如果没有这两步,这种简单的反转操作会退化到需要靠key映射表来一个个查找和移动,效率更低。
-
为什么最后才用 key 映射?
前四种比较都是 “推测性”的快捷方式,它们基于一个假设:位置相近的节点更可能是同一个节点。这个假设在大多数用户操作(如增删首尾项、局部排序)下都是成立的,所以能快速解决大部分问题。
只有当四种快捷比较都失败时,才意味着中间部分可能存在复杂的乱序重排。这时,算法才启用“终极方案”:通过 key 建立的新旧节点映射表来精确查找。这是一种用空间(一个Map)换时间(O(1)查找) 的策略,确保了即使最混乱的情况也能正确处理。
vue3
diff
Vue3 的 Diff 算法移除了 Vue2 中的“头尾比较”和“尾头比较”
在 Vue2 中,头尾/尾头比较主要处理的是 “节点位置交叉移动” 的场景:
// Vue2 头尾/尾头比较主要优化的场景:
旧列表: [A, B, C, D]
新列表: [D, A, B, C] // 尾头比较会发现 D=D
旧列表: [A, B, C, D]
新列表: [B, C, D, A] // 头尾比较会发现 A=A
问题在于:这种优化只在特定模式的移动中有效,对于更复杂的乱序场景帮助有限。
Vue3 的设计哲学转变
Vue3 的思考:与其用四种特殊比较处理部分场景,
不如用一个统一的强大算法处理所有场景
旧的思路(Vue2):
"让我们猜猜节点可能怎么移动,并为此设计快捷路径"
头头比较 → 节点位置没变
尾尾比较 → 节点位置没变
头尾比较 → 节点从头部移到尾部
尾头比较 → 节点从尾部移到头部
新的思路(Vue3):
"让我们先找出所有确定不动的节点,然后一次性处理好所有移动"
1. 先快速找出首尾确定相同的节点(头头、尾尾)
2. 对中间乱序部分,建立完整映射,用LIS找出最少移动方案
Vue3 完全依赖 key 来识别节点身份,而 Vue2 的 sameVnode 除了 key 还考虑标签名等其他因素。
// Vue3 的节点标识更加严格
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key
// 只比较类型和key,比Vue2更简单但更依赖key
}
这意味着:
1. key 在 Vue3 中变得更加重要
2. 没有 key 或 key 不稳定时,Vue3 会直接创建新节点
3. 有了稳定的 key,Vue3 可以建立精确映射,不再需要猜测性比较
头尾/尾头比较是启发式算法(基于经验猜测),而 LIS(最长递增子序列)提供的是数学上的最优解。
// 比较两种策略:
const strategies = {
vue2: {
approach: "启发式比较",
movesForReverse: "n-1 次移动",
optimal: "不是理论最少移动",
complexity: "实现简单"
},
vue3: {
approach: "LIS 优化",
movesForReverse: "约 n/2 次移动",
optimal: "接近理论最少移动",
complexity: "实现复杂但更优"
}
}
// 示例:8个节点的反转
// Vue2: 需要 7 次移动
// Vue3 (LIS): 只需要 4 次移动(优化 ~43%)
Vue3 的编译时优化减少了运行时需要 Diff 的场景(靶向更新):
// Vue3 编译时优化减少了乱序发生的可能性
const optimizations = [
"静态提升(静态节点不参与Diff)",
"Patch Flags(标记动态部分,跳过静态比较)",
"树结构压平(减少嵌套Fragment的Diff层级)"
]
实际效果:很多在 Vue2 中需要完整 Diff 的场景,
在 Vue3 中可能被优化掉,根本不需要进入复杂 Diff
最长低增子序列 26.最长递增子序列概念_哔哩哔哩_bilibili