一文读懂《Vue2 Diff算法》

序言

通过阅读本文章,您可以收获以下内容:

  • 什么是 diff 算法
  • diff 算法的作用
  • diff 算法的原理
  • diff 算法应用场景
  • diff 算法源码实现解析

一、什么是 diff 算法

diff 算法是用于比较两颗虚拟 DOM 树的差异,并生成最小化 DOM 操作指令的算法。它的核心目标是高效更新视图,避免全量渲染的性能浪费。

虚拟 DOM:用 Javascript 对象来描述真实 DOM 结构(如 Vue 中的 VNode)

二、diff 算法的作用

  1. 性能优化减少真实 DOM 的操作次数,降低浏览器重绘回流的开销
  2. 精准更新仅对发生变化的节点进行 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 的响应式数据(如dataprops)变化时,触发虚拟 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
}

关键逻辑解析

  1. ‌ 快速匹配失败后的查找复用 ‌
  • 哈希表构建 ‌:通过 createKeyToOldIdx 生成旧节点的 { key: index } 映射,时间复杂度 ‌O(n)‌。
  • 精准查找 ‌:用新节点的 key 直接定位旧节点位置(oldKeyToIdx[newKey]),时间复杂度 ‌O(1)‌。
  • 无 key 回退 ‌:如果新节点无 key,Vue 会遍历旧节点数组(时间复杂度退化为 ‌O(n)‌),逐个对比 tag 和组件类型。
  1. ‌ 节点移动与状态标记 ‌
  • ‌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)‌:
    将静态节点提升到渲染函数外部,避免重复创建。

七、总结

  1. diff 算法是一种对比虚拟 DOM 树的差异,计算最小 DOM 更新步骤的算法。
  2. diff 算法只在同层级比较,不跨层级比较
  3. diff 算法过程中,先使用头尾双指针 进行两端对比,再配合查找复用法匹配节点
  4. diff 算法中的key比较重要,既能标识节点的唯一性,也能生成哈希表提升复用效率。
相关推荐
Larcher1 天前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐1 天前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭1 天前
如何理解HTML语义化
前端·html
jump6801 天前
url输入到网页展示会发生什么?
前端
诸葛韩信1 天前
我们需要了解的Web Workers
前端
brzhang1 天前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 天前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 天前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 天前
场景模拟:基础路由配置
前端
六月的可乐1 天前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程