序言
通过阅读本文章,您可以收获以下内容:
- 什么是 diff 算法
- diff 算法的作用
- diff 算法的原理
- diff 算法应用场景
- diff 算法源码实现解析
一、什么是 diff 算法
diff 算法是用于比较两颗虚拟 DOM 树的差异,并生成最小化 DOM 操作指令的算法。它的核心目标是高效更新视图,避免全量渲染的性能浪费。
虚拟 DOM:用 Javascript 对象来描述真实 DOM 结构(如 Vue 中的 VNode)
二、diff 算法的作用
- 性能优化减少真实 DOM 的操作次数,降低浏览器重绘 和回流的开销
- 精准更新仅对发生变化的节点进行 DOM 操作,复用未变化的节点(如列表顺序的调整)
三、diff 算法原理
1. 同层比较
- 策略:仅对比新旧虚拟 DOM 树中的同一层级的节点,不跨层级比较
- 原因:跨层级操作在实际开发中极少出现,牺牲这部分覆盖率以换取时间复杂度优化。
- 示例:
html
<!-- 旧结构 --> <!-- 新结构 -->
<div> <section>
<p></p> <p></p>
</div> </section>
此处 <div>
和 <section>
不同标签,直接销毁旧节点并创建新节点
2. 双端对比(头尾指针法)
在对比同一层级子节点数组时,使用头尾双指针向中间移动,快速识别顺序变化。具体步骤如下:
注意:此处总共会创建 4 个指针,分别是旧节点数组的头指针(以下简称
旧头
)和尾指针(以下简称旧尾
),新节点数组的头指针(以下简称新头
)和尾指针(以下简称新尾
)。
- 4 次快速匹配
若匹配成功,则更新旧节点的位置,并移动指针。旧节点移动后会将原位置标记为undefined
diff
- 旧头 vs 新头
- 旧尾 vs 新尾
- 旧头 vs 新尾
- 旧尾 vs 新头
- 查找复用法
当 4 次快速匹配没成功后,会执行查找复用逻辑,此时依赖于子节点设置的key
值,通过key
值生成哈希表快速查找可复用的节点,具体流程如下:
遍历旧节点数组,以key
为键,索引为值的哈希表keyToOldIdx
, 用于快速查找是否存在可复用的旧节点
以新子节点的key
为键,通过哈希表keyToOldIdx
查找是否存在匹配的旧节点。找到可复用节点,则将该旧子节点移动到新的位置,并在旧数组中将其标记为undefined
(后续遍历直接跳过该位置,避免重复处理);未找到可复用的节点,则创建新节点并插入当前新头指针的位置。
markdown
- 生成旧子节点的`key`哈希表
- 遍历新子节点查找复用
3. key
的作用
- 唯一标识:
key
帮助 diff 算法识别节点的唯一性,避免因顺序变化导致的错误复用。 - 方便查找复用:通过
key
生成哈希表,后续通过keyToOldIdx[key]
查找是否存在匹配的旧节点。 - 无
key
的后果:列表顺序变化可能导致节点被错误复用
四、diff 算法应用场景
1. 响应式数据发送变化
当 Vue 的响应式数据(如data
、props
)变化时,触发虚拟 DOM 重新渲染,diff 算法计算差异并更新视图。
2. 组件更新
父组件重新渲染导致子组件更新时,通过 Diff 判断子组件是否需要复用或销毁。
3. 列表渲染(v-for
)
处理动态列表的增删改操作时,依赖 key 值优化节点复用逻辑,通过 diff 算法计算出需要更新的节点。
五、源码实现解析(Vue 2.x)
以 Vue 2 的 src/core/vdom/patch.js
中的 updateChildren
函数为例:
patch.js
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld
// 循环对比新旧节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 1. 四次快速匹配(代码已简化)
if (sameVnode(oldStartVnode, newStartVnode)) { /* 头头匹配 */ }
else if (sameVnode(oldEndVnode, newEndVnode)) { /* 尾尾匹配 */ }
else if (sameVnode(oldStartVnode, newEndVnode)) { /* 头尾匹配 */ }
else if (sameVnode(oldEndVnode, newStartVnode)) { /* 尾头匹配 */ }
else {
// 2. 四次匹配失败,进入查找复用逻辑
if (!oldKeyToIdx) {
// 生成旧节点的 key -> index 哈希表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 用新节点的 key 查找旧节点
idxInOld = newStartVnode.key != null
? oldKeyToIdx[newStartVnode.key]
: null
if (idxInOld == null) {
// 3. 未找到可复用节点,创建新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm)
} else {
// 4. 找到可复用节点
const vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 更新节点属性
patchVnode(vnodeToMove, newStartVnode)
// 移动 DOM 到新位置(插入到当前 newStartIdx 位置)
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 标记旧节点为 undefined(防止后续重复处理)
oldCh[idxInOld] = undefined
} else {
// key 相同但节点不同,强制创建新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
}
// 生成 key -> index 哈希表
function createKeyToOldIdx(children, beginIdx, endIdx) {
const map = {}
for (let i = beginIdx; i <= endIdx; i++) {
const key = children[i]?.key
if (key != null) map[key] = i
}
return map
}
关键逻辑解析
- 快速匹配失败后的查找复用
- 哈希表构建 :通过 createKeyToOldIdx 生成旧节点的 { key: index } 映射,时间复杂度 O(n)。
- 精准查找 :用新节点的 key 直接定位旧节点位置(oldKeyToIdx[newKey]),时间复杂度 O(1)。
- 无 key 回退 :如果新节点无 key,Vue 会遍历旧节点数组(时间复杂度退化为 O(n)),逐个对比 tag 和组件类型。
- 节点移动与状态标记
- DOM 移动 :通过 insertBefore 将复用的 DOM 元素插入到 newStartIdx 对应位置(即当前新子节点的起始位置)。
- 旧节点标记 :将 oldCh[idxInOld] 设为 undefined,后续遍历旧数组时直接跳过该位置(isUndef 判断)。
- 复用优化 :若新旧节点 key 相同但 tag 不同,强制销毁旧节点并创建新节点
六、扩展问题
为什么不建议在v-for
上使用index
作为key
?
- 影响复用节点判断
当列表进行增删操作时,index
随数组的顺序变化而变化,会导致错误的复用旧节点(如:输入框内容错位)
示例:
初始列表 [A, B, C] 的 index 为 0,1,2,若头部插入新元素变为 [D, A, B, C],原 A/B/C 的 index 变为 1,2,3,Vue 会将旧 A 与新 D 的 index=0 对比,误判需更新内容而非复用节点
- 性能浪费
每次列表变动都会导致 index 变化,触发大量节点更新而非复用,增加 DOM 操作次数(如删除头部元素后,所有后续元素需重新渲染)
说说 vue3 的 diff 算法有什么不同,在哪些地方做了改进?
- Block Tree 优化 :
将模板划分为静态块(Block)和动态块,减少动态内容的对比范围。 - Patch Flags
标记动态节点类型(如 CLASS、STYLE),直接定位需更新的属性。 - 静态提升(Hoist Static):
将静态节点提升到渲染函数外部,避免重复创建。
七、总结
- diff 算法是一种对比虚拟 DOM 树的差异,计算最小 DOM 更新步骤的算法。
- diff 算法只在同层级比较,不跨层级比较
- diff 算法过程中,先使用头尾双指针 进行两端对比,再配合查找复用法匹配节点
- diff 算法中的
key
比较重要,既能标识节点的唯一性,也能生成哈希表提升复用效率。