Skip to content

diff 算法 ​

核心 diff 算法只关心新旧 vnode 都存在一组子节点的情况(p218)

虚拟节点数在创建的过程,也在创建对应的 dom 元素,在 vnode 上有一个 el 属性用于保存真实的 dom 节点。

为什么要使用 diff 算法而不是将所有就得节点都卸载掉重新挂载新的节点?因为 web 页面创建太多 dom 元素存在性能瓶颈,diff 目的是尽可能减少开销提高性能

diff 算法进行节点更新时,主要目的在于其对应的真实 dom 而不是虚拟节点本身,本质上是更新前后节点的差异来表示不同,并作用到对应的真实节点上。减少 dom 节点的创建,提高性能。

关于 key ​

虚拟节点中使用 key 作为唯一标识,当 key 与 vnode.type 相同时,认为新旧 vnode 相同可以进行 DOM 复用。注意,DOM 复用并不意味不需要更新

ts
// packages/runtime-core/src/vnode.ts:339

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

diff 流程 ​

响应式数据发生改变 render 函数产生新的 vnode 遍历:遍历新旧节点通过 key 找到对应 vnode 更新:新旧 vnode 进行 patch 更新 DOM 内容 移动:移动节点是指移动一个虚拟节点对应的真实 DOM 节点。每次寻找可复用节点时,会记录该可复用节点在旧的一组子节点中的索引。将旧节点的真实 DOM 赋给新节点的 vnode.el(复用 DOM 节点)

简单 diff 算法 ​

关于判断是否要进行移动: 存储 lastIndex 具体为新 vnode 在旧 vnode 中最大索引,默认值为 0,若后续新 vnode 在旧 vnode 的索引值小于该值,则说明需要进行移动(p228)

移动: 具体移动方式为,如果找不到新 vnode 的前一个节点则说明他是第一个不需要移动(p232),否则将其移动到前一个 vnode 的真实节点后面,具体实现是 insertBefore,需要找到前一个真实节点的兄弟元素作为锚点

添加新元素: 直接挂载到新 vnode 的真实 DOM 下

移除旧元素: 当基本更新对比完后,遍历旧的一组 vnode 在新的一组 vnode 中是否存在,不存在这移除

快速 diff 算法 ​

快速 diff 算法借鉴了纯文本 diff 算法中预处理步骤

关于预处理 ​

对于新旧 vnode 组中,相同的前置和后置节点只需要进行 patch 打补丁(p273),无需进行 DOM 节点移动

具体实现:使用变量 j 指向两组节点的开头,通过isSameVNodeType判断是否为同一个 vnode,是则进行 patch 预处理并且 j++,否则通过 newEnd 和 oldEnd 指向新旧 vnode 组结尾,进行遍历并在循环中递减 1,直至不相同节点为止,此时预处理完成

判断是否进行 DOM 移动(p282) ​

首先,我们使用 source 记录旧节点组在新节点组中索引,默认值为-1,稍后用于计算最长递增子序列(为不需要移动 DOM 的虚拟节点)。

另外,需要一个 patched 标记已更新的节点,多余的 vnode 将卸载

移动(p290) ​

通过 source 求得最长递增子序列 seq,变量 s 为 seq 索引,i 为新的子节点索引,均从末尾开始。对新 vnode 组进行遍历,seq 均向前移动直至为 0。 判断逻辑: if (source[i] === -1)则说明为新增节点执行挂载 if (source[i] === seq[s])存在于最长递增子序列中不需要移动 else 说明该节点非新增节点,并且不存在与最长递增子序列中需要进行移动,移动方式与简单 diff 算法中类似

Released under the MIT License.