Vue 中的 Diff 算法 (差异算法) 是其 虚拟 DOM (Virtual DOM) 实现的核心部分,用于高效地更新真实的 DOM。它的目标是在组件的状态(data、props 等)发生变化时,计算出最小的必要 DOM 操作,从而避免昂贵的全量 DOM 渲染,提升性能。
核心思想
- 同级比较: Diff 算法只会在同一层级的虚拟 DOM 节点之间进行比较,不会跨层级比较。这大大减少了比较的复杂度。如果发现节点跨层级移动了(例如从父节点直接移动到子节点位置),Vue 会直接销毁旧节点并创建新节点,而不是尝试移动它。
- 双端比较: Vue 2.x 的 Diff 核心策略是双端比较 (双指针比较)。算法会同时从新旧子节点列表的头部 (start) 和尾部 (end) 开始向中间进行遍历比较。
- Key 的重要性: 为列表中的元素提供一个稳定且唯一的
key属性至关重要。key是 Vue 识别节点身份的唯一标识符。有了key,Vue 就能在列表顺序改变时,高效地复用已有的 DOM 节点(移动它们),而不是销毁重建。没有key或使用不稳定的key(如index)会导致低效的更新(大量不必要的销毁/重建)或状态错误。 - 就地复用: 如果算法判断两个新旧节点是同一个节点(基于
key和sel/tag等),Vue 会尝试复用该节点对应的真实 DOM 元素,并仅更新该节点上发生变化的属性/内容/子节点,而不是销毁重建。
Vue Diff 算法的基本步骤(以子节点列表更新为例)
当比较新旧虚拟节点的子节点列表 (children) 时:
- 新旧头指针比较: 比较
newStartVnode和oldStartVnode。- 如果相同:复用节点,更新内容,新旧头指针都后移一位。
- 如果不同:进入下一步。
- 新旧尾指针比较: 比较
newEndVnode和oldEndVnode。- 如果相同:复用节点,更新内容,新旧尾指针都前移一位。
- 如果不同:进入下一步。
- 旧头新尾比较: 比较
oldStartVnode和newEndVnode。- 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前
oldEndVnode之后,oldStartVnode指针后移,newEndVnode指针前移。 - 如果不同:进入下一步。
- 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前
- 旧尾新头比较: 比较
oldEndVnode和newStartVnode。- 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前
oldStartVnode之前,oldEndVnode指针前移,newStartVnode指针后移。 - 如果不同:进入下一步。
- 如果相同:复用节点,更新内容,然后将该节点对应的真实 DOM 移动到当前
- Key 映射查找:
- 如果以上四种快速比较都没有命中,Vue 会尝试利用
key进行查找。 - 它会在旧子节点中查找是否存在一个节点,其
key与newStartVnode相同。 - 如果找到了 (
idxInOld):- 检查新旧节点是否真的是同一个节点(
sel/tag也要匹配)。 - 如果是:复用该节点对应的真实 DOM,更新内容,然后将该真实 DOM 移动到当前
oldStartVnode之前。 - 将
oldChildren[idxInOld]置为undefined(标记为已处理,避免重复处理)。
- 检查新旧节点是否真的是同一个节点(
- 如果没找到:说明
newStartVnode是一个新节点,创建对应的真实 DOM 并插入到当前oldStartVnode之前。 newStartVnode指针后移一位。
- 如果以上四种快速比较都没有命中,Vue 会尝试利用
- 循环结束处理:
- 当
newStartIdx > newEndIdx:说明新子节点列表处理完了。循环移除oldStartIdx到oldEndIdx之间剩余的旧节点(这些节点在新列表中不存在了)。 - 当
oldStartIdx > oldEndIdx:说明旧子节点列表处理完了。循环将newStartIdx到newEndIdx之间剩余的新节点创建并插入到末尾(通常是oldEndVnode对应的真实 DOM 之后)。
- 当