diff算法

147 阅读3分钟

Vue 中的 Diff 算法 (差异算法) 是其 虚拟 DOM (Virtual DOM) 实现的核心部分,用于高效地更新真实的 DOM。它的目标是在组件的状态(dataprops 等)发生变化时,计算出最小的必要 DOM 操作,从而避免昂贵的全量 DOM 渲染,提升性能。

核心思想

  1. 同级比较: Diff 算法只会在同一层级的虚拟 DOM 节点之间进行比较,不会跨层级比较。这大大减少了比较的复杂度。如果发现节点跨层级移动了(例如从父节点直接移动到子节点位置),Vue 会直接销毁旧节点并创建新节点,而不是尝试移动它。
  2. 双端比较: Vue 2.x 的 Diff 核心策略是双端比较 (双指针比较)。算法会同时从新旧子节点列表的头部 (start)尾部 (end) 开始向中间进行遍历比较。
  3. Key 的重要性: 为列表中的元素提供一个稳定且唯一的 key 属性至关重要。key 是 Vue 识别节点身份的唯一标识符。有了 key,Vue 就能在列表顺序改变时,高效地复用已有的 DOM 节点(移动它们),而不是销毁重建。没有 key 或使用不稳定的 key(如 index)会导致低效的更新(大量不必要的销毁/重建)或状态错误。
  4. 就地复用: 如果算法判断两个新旧节点是同一个节点(基于 keysel/tag 等),Vue 会尝试复用该节点对应的真实 DOM 元素,并仅更新该节点上发生变化的属性/内容/子节点,而不是销毁重建。

Vue Diff 算法的基本步骤(以子节点列表更新为例)

当比较新旧虚拟节点的子节点列表 (children) 时:

  1. 新旧头指针比较: 比较 newStartVnodeoldStartVnode
    • 如果相同:复用节点,更新内容,新旧头指针都后移一位。
    • 如果不同:进入下一步。
  2. 新旧尾指针比较: 比较 newEndVnodeoldEndVnode
    • 如果相同:复用节点,更新内容,新旧尾指针都前移一位。
    • 如果不同:进入下一步。
  3. 旧头新尾比较: 比较 oldStartVnodenewEndVnode
    • 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前 oldEndVnode 之后oldStartVnode 指针后移,newEndVnode 指针前移。
    • 如果不同:进入下一步。
  4. 旧尾新头比较: 比较 oldEndVnodenewStartVnode
    • 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前 oldStartVnode 之前oldEndVnode 指针前移,newStartVnode 指针后移。
    • 如果不同:进入下一步。
  5. Key 映射查找:
    • 如果以上四种快速比较都没有命中,Vue 会尝试利用 key 进行查找。
    • 它会在旧子节点中查找是否存在一个节点,其 keynewStartVnode 相同。
    • 如果找到了 (idxInOld)
      • 检查新旧节点是否真的是同一个节点(sel/tag 也要匹配)。
      • 如果是:复用该节点对应的真实 DOM,更新内容,然后将该真实 DOM 移动到当前 oldStartVnode 之前
      • oldChildren[idxInOld] 置为 undefined(标记为已处理,避免重复处理)。
    • 如果没找到:说明 newStartVnode 是一个新节点,创建对应的真实 DOM 并插入到当前 oldStartVnode 之前。
    • newStartVnode 指针后移一位。
  6. 循环结束处理:
    • newStartIdx > newEndIdx:说明新子节点列表处理完了。循环移除 oldStartIdxoldEndIdx 之间剩余的旧节点(这些节点在新列表中不存在了)。
    • oldStartIdx > oldEndIdx:说明旧子节点列表处理完了。循环将 newStartIdxnewEndIdx 之间剩余的新节点创建并插入到末尾(通常是 oldEndVnode 对应的真实 DOM 之后)。