一文读懂《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比较重要,既能标识节点的唯一性,也能生成哈希表提升复用效率。
相关推荐
2501_915373884 小时前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
沙振宇7 小时前
【Web】使用Vue3开发鸿蒙的HelloWorld!
前端·华为·harmonyos
运维@小兵8 小时前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿8 小时前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh8 小时前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
一口一个橘子8 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦9 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
香蕉可乐荷包蛋10 小时前
vue数据可视化开发echarts等组件、插件的使用及建议-浅看一下就行
vue.js·信息可视化·echarts
老马啸西风10 小时前
sensitive-word-admin v2.0.0 全新 ui 版本发布!vue+前后端分离
vue.js·ui·ai·nlp·github·word
湛海不过深蓝10 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js