vuejs 设计与实现 - 双端diff算法

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

1.双端比较的原理

双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。因此,我们需要四个索引值,分别指向新旧两组子节点的端点.如下图:

请添加图片描述

双端比较的方式:

请添加图片描述
在双端比较中,每一轮比较都分为四个步骤,如图 10-5 中的连线所示。
比较的过程如下描述:
第一步: 比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 p-4,看看它们是否相同。由于两者的key 值不同,因此不相同,不可复用,于是什么都不做。

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

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

第四步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的第一个子节点 p-4。由于它们的 key 值相同,因此可以进行 DOM 复用。
请添加图片描述

 function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)
}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}}

第一轮 DOM 移动操作完成 态状的点节后,新旧两组子节点以及真实 DOM 节点的状态如下:
请添加图片描述

此时,真实 DOM 节点顺序为 p-4、p-1、p-2、p-3,这与新的 一组子节点顺序不一致。这是因为diff算法还没结束,还需要进行下一轮更新。因此,我们需要将更新逻辑封装到一个 while 循环中,

function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]+           while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}+           }}

由于在每一轮更新完成之后,紧接着都会更新四个索引中与当前更新轮次相关联的索引,所以整个 while 循环执行的条件是:头部索引值要小于等于尾部索引值。

在第一轮更新结束后循环条件仍然成立,因此需要进行下一轮的比较:

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

这里,我们使用了新的名词: 。它指的是头部索引oldStartIdx 和 newStartIdx 所指向的节点。

第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。另外,由于两者都处于尾部,因此不需要对真实 DOM 进行移动操作,只需要打补丁即可:


function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁+              patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量+             oldEndVNode = oldChildren[--oldEndIdx]+             newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}}}

请添加图片描述
真实 DOM 的顺序相比上一轮没有变化,因为在这一轮的比较中没有对 DOM 节点进行移动,只是对 p-3 节点打补丁。接下来,我们再根据图 上图所示的状态执行下一轮的比较:

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

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

第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-1。两者的 key 值相同,可以复用。

在第三步的比较中,我们找到了相同的节点,这说明: p-1原本是头部节点,但是在新的顺序中,它变成了尾部节点。因此,我们需要将节点p-1对应的真实 DOM 移动到旧的一组子节点的尾部节点 p-2 所对应的真实 DOM 后面,同时还需要更新相应的索引到下一个位置,如图 下图所示:
请添加图片描述

function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较+             patch(oldStartVNode, newEndVNode, container)+             insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)+             oldStartVNode = oldChildren[++oldStartIdx]+             newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}}}

下一轮循环:
第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组 子节点中的头部节点 p-2。发现两者 key 值相同,可以复用。但 两者在新旧两组子节点中都是头部节点,因此不需要移动,只需 要调用 patch 函数进行打补丁即可。

function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁+                 patch(oldStartVNode, newStartVNode, container)// 更新相关索引,指向下一个位置+                 oldStartVNode = oldChildren[++oldStartIdx]+                 newStartVNode = newChildren[++newStartIdx]} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}}}

在这一轮更新之后,新旧两组子节点与真实 DOM 节点的状态如图下图 10-10 所示。

请添加图片描述

双端比较的优势

优势:减少移动操作。

案例分析:如下图的新旧两组子节点:
请添加图片描述

简单diff:移动两次
请添加图片描述

双端diff:移动一次

请添加图片描述

非理想状态的处理方式

第一轮都无法命中

  • 旧的一组子节点:p-1、p-2、p-3、p-4。
  • 新的一组子节点:p-2、p-4、p-1、p-3。

当我们尝试按照双端 Diff 算法的思路进行第一轮比较时,会发现无法命中四个步骤中的任何一步。这个时候怎么办呢?这时,我们只能通过增加额外的处理步骤来处理这种非理想情况。既然两个头部和两个尾部的四个节点中都没有可复用的节点,那么我们就尝试看看非头部、非尾部的节点能否复用。具体做法是,拿新的一组子节点中的头部节点去旧的一组子节点中寻找:如下面的代码:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引,指向下一个位置oldStartVNode = oldChildren[++oldStartIdx]newStartVNode = newChildren[++newStartIdx]} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]} else {
+					// 处理非理想情况// 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引+                   const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)}}

如下图在旧子节点中寻找可复用节点:
请添加图片描述

function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren = n1.children const newChildren = n2.children// 四个索引值let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引,指向下一个位置oldStartVNode = oldChildren[++oldStartIdx]newStartVNode = newChildren[++newStartIdx]} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}  else {// 处理非理想情况// 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)// idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部+                   if(idxInOld > 0) {+                       // idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove = oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁+                       patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
+                        insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefined+                       oldChildren[idxInOld] = undefined// 最后更新 newStartIdx 到下一个位置+                       newStartVNode = newChildren[++newStartIdx]}}}}

在上面这段代码中,首先判断 idxInOld 是否大于 0。如果条件 成立,则说明找到了可复用的节点,然后将该节点对应的真实 DOM 移 动到头部。为此,我们先要获取需要移动的节点,这里的 oldChildren[idxInOld] 所指向的节点就是需要移动的节点。在移 动节点之前,不要忘记调用 patch 函数进行打补丁。接着,调用 insert 函数,并以现在的头部节点对应的真实 DOM 节点 oldStartVNode.el 作为锚点参数来完成节点的移动操作。当节点移 动完成后,还有两步工作需要做:

    1. 由于处于 idxInOld 处的节点已经处理过了(对应的真实 DOM 移到了别处),因此我们应该将 oldChildren[idxInOld] 设 置为undefined。
    1. 新的一组子节点中的头部节点已经处理完毕,因此将 newStartIdx 前进到下一个位置。

经过上述两个步骤的操作后,新旧两组子节点以及真实 DOM 节点 的状态如图 下图所示:
请添加图片描述
此时,真实 DOM 的顺序为:p-2、p-1、p-3、p-4。接着,双端 Diff 算法会继续进行。如下图所示:
请添加图片描述

第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者 key 值不同,不可复用。
第二步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的尾部节点 p-3,两者 key 值不同,不可复用。
第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-3,两者 key 值不同,不可复用。
第四步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的头部节点 p-4,两者的 key 值相同,可以复用。

在这一轮比较的第四步中,我们找到了可复用的节点。因此,按照双端 Diff 算法的逻辑移动真实 DOM,即把节点 p-4 对应的真实DOM 移动到旧的一组子节点中头部节点 p-1 所对应的真实 DOM 前面,如图 下图 所示:

请添加图片描述

此时,真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着,开始下一轮的比较:
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1,两者的 key 值相同,可以复用。

在这一轮比较中,第一步就找到了可复用的节点。由于两者都处于头部,所以不需要对真实 DOM 进行移动,只需要打补丁即可。在这一步操作过后,新旧两组子节点与真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

此时,真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着,进行下一轮的比较。需要注意的一点是,此时旧的一组子节点的
头部节点是 undefined。这说明该节点已经被处理过了,因此不需要再处理它了,直接跳过即可。为此,我们需要补充这部分逻辑的代码,具体实现如下:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 增加两个判断分支,如果头尾部节点为 undefined,则说明该节点已经被处理过了,直接跳到下一个位置+ if (!oldStartVNode) {+     oldStartVNode = oldChildren[++oldStartIdx]+ } else if (!oldEndVNode) {+ 	 oldEndVNode = oldChildren[--oldEndIdx]+ }else if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引,指向下一个位置oldStartVNode = oldChildren[++oldStartIdx]newStartVNode = newChildren[++newStartIdx]} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}  else {// 处理非理想情况// 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)// idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部if(idxInOld > 0) {// idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove = oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefinedoldChildren[idxInOld] = undefined// 最后更新 newStartIdx 到下一个位置newStartVNode = newChildren[++newStartIdx]}}
}

观察上面的代码,在循环开始时,我们优先判断头部节点和尾部节点是否存在。如果不存在,则说明它们已经被处理过了,直接跳到下一个位置即可。在这一轮比较过后,新旧两组子节点与真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

现在,四个步骤又重合了,接着进行最后一轮的比较:
第一步:比较旧的一组子节点中的头部节点 p-3 与新的一组子节点中的头部节点 p-3,两者的 key 值相同,可以复用。在第一步中找到了可复用的节点。由于两者都是头部节点,因此不需要进行 DOM 移动操作,直接打补丁即可。在这一轮比较过后,最终状态如图 下图 所示:
请添加图片描述
这时,满足循环停止的条件,于是更新完成。最终,真实 DOM 节点的顺序与新的一组子节点的顺序一致,都是:p-2、p-4、p-1、p-3。

添加新元素

添加新元素的时机:1.四个步骤的比较中都找不到可复用的节点 。 2.尝试拿新的一组子节点中的头部节点 p-4 去旧的一组子节点中寻找具有相同 key 值的节点,但在旧的一组子节点中根本就没有 p-4 节点。这说明节点 p-4 是一个新增节点。

案例1如下:

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-4、p-1、p-3、p-2。
    请添加图片描述

代码如下:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (!oldStartVNode) {oldStartVNode = oldChildren[++oldStartIdx]} else if (oldStartVNode.key === newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引,指向下一个位置oldStartVNode = oldChildren[++oldStartIdx]newStartVNode = newChildren[++newStartIdx]} else if (oldEndVNode.key === newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]} else if(oldStartVNode.key === newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]} else if (oldEndVNode.key === newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。// 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后,更新索引值,指向下一个位置oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}  else {// 处理非理想情况// 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)// idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部if(idxInOld > 0) {// idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove = oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefinedoldChildren[idxInOld] = undefined// 最后更新 newStartIdx 到下一个位置newStartVNode = newChildren[++newStartIdx]} else {+          				// 新增节点
+           			// 将 newStartVNode 作为新节点挂载到头部,使用当前头部节点oldStartVNode.el 作为锚点
+           			patch(null, newStartVNode, container, oldStartVNode.el)}}}

当条件idxInOld > 0不成立时,说明 newStartVNode 节点是全新的节点。又由于 newStartVNode 节点 是头部节点,因此我们应该将其作为新的头部节点进行挂载。所以, 在调用 patch 函数挂载节点时,我们使用 oldStartVNode.el 作为 锚点。在这一步操作完成之后,新旧两组子节点以及真实 DOM 节点的 状态如下图所示:
请添加图片描述

案例2

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-4、p-1、p-2、p-3。
    第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
    第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。
    在第二步中找到了可复用的节点,因此进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图下图 所示:
    请添加图片描述
    接着进行下一轮的比较:
    第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
    第二步:比较旧的一组子节点中的尾部节点 p-2 与新的一组子节点中的尾部节点 p-2,两者的 key 值相同,可以复用。
    我们又在第二步找到了可复用的节点,于是再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示:

请添加图片描述
接着,进行下一轮的更新:
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
第二步:比较旧的一组子节点中的尾部节点 p-1 与新的一组子节点中的尾部节点 p-1,两者的 key 值相同,可以复用。

还是在第二步找到了可复用的节点,再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

当这一轮更新完毕后,由于变量 oldStartIdx 的值大于oldEndIdx 的值,满足更新停止的条件,因此更新停止。但通过观察可知,节点 p-4 在整个更新过程中被遗漏了,没有得到任何处理,这说明我们的算法是有缺陷的。为了弥补这个缺陷,我们需要添加额外的处理代码:

 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 省略部分代码}// 循环结束后检查索引值的情况,
+  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
+ 	// 如果满足条件,则说明有新的节点遗留,需要挂载它们
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
+      patch(null, newChildren[i], container, oldStartVNode.el)+  }}

我们在 while 循环结束后增加了一个 if 条件语句,检查四个索引值的情况。根据图上图可知,如果条件oldEndIdx <oldStartIdx && newStartIdx <= newEndIdx成立,说明新的一组子节点中有遗留的节点需要作为新节点挂载。哪些节点是新节点呢?索引值位于 newStartIdx 和 newEndIdx 这个区间内的节点都是新节点。``于是我们开启一个 for 循环来遍历这个区间内的节点并逐一挂载。挂载时的锚点仍然使用当前的头部节点oldStartVNode.el,这样就完成了对新增元素的处理。

移除不存在的元素

案例如下:

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-1、p-3。
    请添加图片描述

可以看到,在新的一组子节点中 p-2 节点已经不存在了。为了搞清楚应该如何处理节点被移除的情况,我们还是按照双端 Diff 算法的思路执行更新。
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1,两者的 key 值相同,可以复用。
在第一步的比较中找到了可复用的节点,于是执行更新。在这一轮比较过后,新旧两组子节点以及真实 DOM 节点的状态如图下图所示:
请添加图片描述
接着,执行下一轮更新:
第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组子节点中的头部节点 p-3,两者的 key 值不同,不可以复用。
第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。

在第二步中找到了可复用的节点,于是进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图所示:
请添加图片描述
此时变量 newStartIdx 的值大于变量 newEndIdx 的值,满足更新停止的条件,于是更新结束。但观察图 10-34 可知,旧的一组子节点中存在未被处理的节点,应该将其移除。因此,我们需要增加额外的代码来处理它,如下所示:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 省略部分代码}// 循环结束后检查索引值的情况,
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {// 如果满足条件,则说明有新的节点遗留,需要挂载它们for (let i = newStartIdx; i <= newEndIdx; i++) {patch(null, newChildren[i], container, oldStartVNode.el)}
+ } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
+ 	 for (let i = oldStartIdx; i <= oldEndIdx; i++) {
+		unmount(oldChildren[i])
+	}}

与处理新增节点类似,我们在 while 循环结束后又增加了一个else…if 分支,用于卸载已经不存在的节点。由图 上图 可知,索引值位于 oldStartIdx 和 oldEndIdx 这个区间内的节点都应该被卸载,于是我们开启一个 for 循环将它们逐一卸载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/28895.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MFC】07.MFC第三大机制:消息映射-笔记

本专栏上两篇文章分别介绍了【MFC】05.MFC第一大机制&#xff1a;程序启动机制和【MFC】06.MFC第二大机制&#xff1a;窗口创建机制&#xff0c;这篇文章来为大家介绍MFC的第三大机制&#xff1a;消息映射 typfd要实现消息映射&#xff0c;必须满足的三个条件&#xff1a; 类必…

未来,运营的重要性大于产品?

微博上看到某产品大V的一个观点&#xff0c;说在未来&#xff0c;产品运营的重要性会大过产品经理&#xff0c;还挺认同的&#xff0c;谈谈我的想法。 这个观点的核心依据是&#xff0c;目前没有新的产品形态&#xff0c;各种产品解决方案都是标准化的&#xff0c;产品由开疆辟…

Java 面试题2023

Java core JVM 1、JVM内存模型 2、JVM运行时内存分配 3、如何确定当前对象是个垃圾 4、GCrooot 包括哪些? 5、JVM对象头包含哪些部分 6、GC算法有哪些 7、JVM中类的加载机制 8、分代收集算法 9、JDK1.8 和 1.7做了哪些优化 10、内存泄漏和内存溢出有什么区别 11、J…

告别传统人肉运维,实现360°可观测!奇点云数据存算引擎DataKun R2.0发布

近日&#xff0c;奇点云的数据云全系产品发布新一季更新。本文着重分享数据存算引擎DataKun、数据安全引擎DataBlack、指标工厂SimbaMetric、标签工厂SimbaTag的最新进展。 摘要&#xff1a; 数据存算引擎DataKun R2.0发布&#xff0c;在安全稳定、智能运维、自主可控等层面均…

深入理解Jdk5引入的Java泛型:类型安全与灵活性并存

深入理解Jdk5引入的Java泛型&#xff1a;类型安全与灵活性并存 ​ 在Java的中&#xff0c;有一个强大的工具&#xff0c;它可以让你在编写代码时既保持类型安全&#xff0c;又享受灵活性。**这个工具就是——泛型&#xff08;Generics&#xff09;。**本文将引导你深入了解Java…

ZZULIOJ 1191: 数星星(结构体专题),Java

ZZULIOJ 1191: 数星星&#xff08;结构体专题&#xff09;&#xff0c;Java 题目描述 一天&#xff0c;小明坐在院子里数星星&#xff0c;Gardon就出了个难题给她&#xff1a;Gardon在天空画出了一个矩形区域&#xff0c;让他输入矩形区域里有多少颗星星&#xff0c;仁慈的上…

如何简单的无人直播

环境搭建 ffmpeg安装&#xff0c;我这里用的是centos搭建的&#xff0c;其他平台可以自己百度 yum -y install wgetwget --no-check-certificate https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xztar -xJf ffmpeg-4.0.3-64bit-static.ta…

袋鼠云数栈 DataOps 数据生产力实践,实现数据流程的自动化和规范化

袋鼠云产品团队在帮助企业进行数字化转型实践的过程中&#xff0c;发现很多企业在数据生产链路上都有着相同的问题。包括数据团队聚焦于业务需求短期内的快速交付&#xff0c;内部缺少自顶向下的数据生产管理制度&#xff0c;在数据标准、数据生产流程到研发规范的各个层面均存…

博为软件远程医疗,助力上级医生给基层病患实时医疗服务

远程医疗解决方案正逐渐成为现代医疗领域的一项重要工具。通过使用远程音视频技术&#xff0c;医生能够在不同的场景下为病患提供及时和高效的医疗服务。 这里例举博为软件远程医疗解决方案中几个常见场景&#xff1a; 1、远程会诊 远程会诊成为医生们互相交流和合作的重要方式…

【网络】数据链路层

目录 一、以太网 二、以太网帧格式 三、 MTU 1、MTU概念 2、 MTU对IP协议的影响 3、MTU对UDP协议的影响 4、 MTU对于TCP协议的影响 四、MAC地址 五、 ARP协议 1、ARP协议的作用 2、ARP协议的工作流程 3、ARP数据报的格式 4、中间人 数据链路层解决的&#xff0c;是…

分享windwosServer2012R--ISO镜像下载地址(含激活教程)

windowsServer2012R----急速网盘下载地址&#xff1a;点击下载 提取码&#xff1a;888999 激活下载&#xff1a;点击下载 提取码&#xff1a;888999

没有synchronized,rust怎么防并发?

学过Java的同学对synchronized肯定不陌生&#xff0c;那么rust里怎么办呢&#xff1f; 在Rust中&#xff0c;可以使用标准库提供的 std::sync::Mutex 来实现加锁功能。Mutex是互斥锁的一种实现&#xff0c;用于保护共享数据在并发访问时的安全性。 下面是一个简单的示例代码&a…

【Linux】网络层、数据链路层、DNS、ICMP协议、NAT技术

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录 &#x1f449;网络层&a…

【香瓜说职场】如何高效地提问(2018.05.06)

一、什么是低效地提问&#xff1f; 香瓜先举3个非常非常常见的低效提问实例&#xff1a; 1、“为什么我的XXX不成功&#xff1f;” 这个问题就像“为什么我会摔倒”&#xff0c;可能原因有“腿残疾”、“路上有坑”、“眼神不好”等无数种原因……“不摔倒”的我是回答不了的、…

一道Java继承与重写的经典问题

话不多说&#xff0c;先上代码 class Base {private String name "base";public Base() {tellName();printName();}public void tellName() {System.out.println("Base tell name: " name);}public void printName() {System.out.println("Base pr…

android nv21数据用mediacodec编解码

在 Android 中使用 MediaCodec 进行 NV21 编码和解码的过程如下&#xff1a; 编码 NV21 数据&#xff1a; // 创建 MediaCodec 编码器&#xff0c;并配置编码器格式和参数 val encoder MediaCodec.createEncoderByType("video/avc") val mediaFormat MediaFormat…

Django入门 - 路由Route的基本使用

文章目录 1. 直接访问视图函数&#xff0c;没有使用子路由2. 使用子路由 urls.py 我们一般叫它根路由 1. 直接访问视图函数&#xff0c;没有使用子路由 MyDjangoPro2\views.py 代码 from django.shortcuts import renderfrom django.http import HttpResponse# 视图函数Views …

消息队列 (9)-消费者核心类的实现

目录 前言消费者类设计思路核心API总体代码 前言 我们上一篇博客,写了虚拟主机的实现, 在虚拟主机中需要用到俩个未实现的类,分别是验证绑定关键字和消费者类,接下来我们实现消费者类的核心代码 消费者类设计思路 在这个类中,首先我们要持有virtualHost对象来操作数据, 然后…

【BASH】回顾与知识点梳理(十)

【BASH】回顾与知识点梳理 十 十. 文件的格式化与相关处理10.1 格式化打印&#xff1a; printf10.2 awk&#xff1a;好用的数据处理工具awk 的逻辑运算字符 10.3 文件比对工具diffcmppatch 10.4 文件打印准备&#xff1a; pr 该系列目录 --> 【BASH】回顾与知识点梳理&#…

移动端开发基础总结

移动端学习总结 (适合于复习) 移动端基础 技术选型&#xff1a; 单独制作移动端页面&#xff08;主流&#xff09; 流式布局&#xff08;百分比布局&#xff09;flex弹性布局&#xff08;强烈推荐&#xff09;lessrem媒体查询布局混合布局 响应式页面兼容移动端&#xff08;…