渲染器——双端Diff算法

简单 Diff 算法利用虚拟节点的 key 属性,尽可能地复用 DOM 元素,并通过移动 DOM 的方式来完成更新,从而减少不断地创建和销毁DOM 元素带来的性能开销。但是,简单 Diff 算法仍然存在很多缺陷,这些缺陷可以通过双端 Diff 算法解决。

1、双端比较的原理

简单 Diff 算法的问题在于,它对 DOM 的移动操作并不是最优的。我们拿上一章的例子来看,如下图所示:
渲染器——双端Diff算法_第1张图片
在这个例子中,如果使用简单 Diff 算法来更新它,则会发生两次 DOM 移动操作,如下图所示:
渲染器——双端Diff算法_第2张图片
第一次 DOM 移动操作会将真实 DOM 节点 p-1 移动到真实DOM 节点 p-3 后面。第二次移动操作会将真实 DOM 节点 p-2 移动到真实 DOM 节点 p-1 后面。最终,真实 DOM 节点的顺序与新的一组子节点顺序一致:p-3、p-1、p-2。

然而,上述更新过程并非最优解。在这个例子中,其实只需要通过一步 DOM 节点的移动操作即可完成更新,即只需要把真实 DOM 节点 p-3 移动到真实 DOM 节点 p-1 前面,如下图所示:
渲染器——双端Diff算法_第3张图片
可以看到,理论上只需要一次 DOM 移动操作即可完成更新。但简单 Diff 算法做不到这一点,不过双端Diff 算法可以做到。接下来,我们就来讨论双端 Diff 算法的原理。

顾名思义,双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。因此,我们需要四个索引值,分别指向新旧两组子节点的端点,如下图所示:
渲染器——双端Diff算法_第4张图片
用代码来表达四个端点,如下面 patchChildren 和patchKeyedChildren 函数的代码所示:

01 function patchChildren(n1, n2, container) {
02   if (typeof n2.children === 'string') {
03     // 省略部分代码
04   } else if (Array.isArray(n2.children)) {
05     // 封装 patchKeyedChildren 函数处理两组子节点
06     patchKeyedChildren(n1, n2, container)
07   } else {
08     // 省略部分代码
09   }
10 }
11
12 function patchKeyedChildren(n1, n2, container) {
13   const oldChildren = n1.children
14   const newChildren = n2.children
15   // 四个索引值
16   let oldStartIdx = 0
17   let oldEndIdx = oldChildren.length - 1
18   let newStartIdx = 0
19   let newEndIdx = newChildren.length - 1
20 }

在上面这段代码中,我们将两组子节点的打补丁工作封装到了patchKeyedChildren 函数中。在该函数内,首先获取新旧两组子节点 oldChildren 和 newChildren,接着创建四个索引值,分别指向新旧两组子节点的头和尾,即 oldStartIdx、oldEndIdx、newStartIdx 和 newEndIdx。有了索引后,就可以找到它所指向的虚拟节点了,如下面的代码所示:

01 function patchKeyedChildren(n1, n2, container) {
02   const oldChildren = n1.children
03   const newChildren = n2.children
04   // 四个索引值
05   let oldStartIdx = 0
06   let oldEndIdx = oldChildren.length - 1
07   let newStartIdx = 0
08   let newEndIdx = newChildren.length - 1
09   // 四个索引指向的 vnode 节点
10   let oldStartVNode = oldChildren[oldStartIdx]
11   let oldEndVNode = oldChildren[oldEndIdx]
12   let newStartVNode = newChildren[newStartIdx]
13   let newEndVNode = newChildren[newEndIdx]
14 }

其中,oldStartVNode 和 oldEndVNode 是旧的一组子节点中的第一个节点和最后一个节点,newStartVNode 和newEndVNode 则是新的一组子节点的第一个节点和最后一个节点。有了这些信息之后,我们就可以开始进行双端比较了。怎么比较呢?如下图所示:
渲染器——双端Diff算法_第5张图片
在双端比较中,每一轮比较都分为四个步骤,如上图中的连线所示:

  • 第一步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 p-4,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。

你可能感兴趣的:(Web,#,Vue+TypeScript,算法,javascript,前端,vue.js)