快速Diff算法-Vue3

快速Diff算法

快速 Diff 算法在实测中性能最优。它借鉴了文本 Diff 算法中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增序列所指向的节点即为不需要移动的节点。

不同于简单Diff算法和双端Diff算法,快速Diff算法包含预处理步骤,这其实是借鉴了纯文本Diff算法的思路。

在纯文本Diff算法中,存在对两段文本进行预处理的过程。例如:在对两段文本进行Diff之前,可以先对他们进行全等比较:

if (text1 === text2) return 

这也称为快捷路径。如果两段文本全等,那么就无需进行核心Diff算法的步骤。除此之外,预处理过程还会处理两段文本相同的前缀和后缀。假设有如下两段文本:

TEXT1: I use vue for app development;
TEXT2: I use react for app development;

这两段文本的头部和尾部分别有一段相同的内容“I use”和“for app development”。因此对于 TEXT1 和 TEXT2 来说,真正需要进行 Diff 操作的部分是

TEXT1: vue
TEXT2: react

以下面两组节点为例:
在这里插入图片描述

通过观察不难发现,两组节点具有相同的前置节点p-1以及相同的后置节点p-2和p-3,如下图左侧所示。对于相同的前置节点和后置节点,由于他们在新旧两组子节点中的相对位置不变,所以我们无需移动他们,但仍然需要在他们之间打补丁。

在这里插入图片描述

对于前置节点,我们可以建立索引 j,其初始值为0,用来指向两组子节点的开头,如上图右侧所示。然后开启一个 while 循环,让索引 j递增,直到遇到不相同的节点为止,如下面 patchKeyChildren 函数的代码所示:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0;let oldVnode = oldChildren[j];let newVNode = newChildren[j];// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止while(oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container);// 更新索引 j,让其递增j++;oldVnode = oldChildren[j];newVNode = newChildren[j];}
}

在上面这段代码中,使用 while 循环查找所有相同的前置节点,并调用 patch 函数进行打补丁,直到遇到 key 值不同的节点为止。这样就完成了对前置节点的更新,更新操作过后,新旧两组子节点的状态如下:

在这里插入图片描述

这里需要注意的是,当 while 循环终止时,索引 j 的值为1。接下来需要处理相同的后置节点。由于新旧两组子节点的数量可能不同,索引我们需要两个索引 newEnd 和 oldEnd,分别指向新旧两组子节点中的最后一个节点,如下图所示:

在这里插入图片描述

我们再开启一个 while 循环,并从后向前遍历这两组子节点,直到遇到 key 值不同的节点为止,如下代码所示:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0;let oldVnode = oldChildren[j];let newVNode = newChildren[j];// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container);// 更新索引 j,让其递增j++;oldVnode = oldChildren[j];newVNode = newChildren[j];}// 更新相同的后置节点// 索引 oldEnd 指向旧的一组子节点的最后一个节点let oldEnd = oldChildren.length - 1;// 索引 newEnd 指向新的一组子节点的最后一个节点let newEnd = newChildren.length - 1;oldVnode = oldChildren[oldEnd];newVNode = newChildren[newEnd];// while 循环从后向前遍历,直到遇到拥有不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container);// 递减 oldEnd 和 newEnd --oldEnd;--newEnd;oldVnode = oldChildren[oldEnd];newVNode = newChildren[newEnd];}
}

与处理相同的前置节点一样,在 while 循环内,需要调用 patch 函数进行打补丁,然后递减两个索引 oldEnd 和 newEnd 的值。在这一步更新操作过后,新旧两组子节点的状态如下:

在这里插入图片描述

由图可知,当相同的前置节点和后置节点被处理完毕后,旧的一组子节点已经被全部处理了,而在新的一组子节点中,还遗留了一个未被处理的节点p-4。其实不难发现,节点p-4是新增节点。那么如何用程序得出“节点p-4是新增节点”这个结论,需要观察 j、newEnd 和 oldEnd之间的关系。

  • 条件一 oldEnd < j 成立:说明在预处理过程中,所有的旧子节点都处理完毕了。
  • 条件二 newEnd >= j 成立:说明在预处理过后,在新的一组子节点中,仍然有未被处理的节点,而这些遗留的节点将被视作新增节点

如果条件一和条件二同时成立,说明在新的一组子节点中,存在遗留节点,且这些节点都是新增节点。因此我们需要将他们挂载到正确的位置。如下图所示:

在这里插入图片描述

在新的一组子节点中,索引值处于 j 和 newEnd 之间的任何节点都需要作为新的子节点进行挂载。如上图可知,锚点元素是p-2节点对应的真实 DOM 前面。具体代码如下:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 更新相同的前置节点// 省略部分代码// 更新相同的后置节点// 省略部分代码// 预处理完毕后,如果满足如下条件,则说明从 j --> newEnd 之间的节点应作为新节点插入if (j > oldEnd && j <= newEnd) {// 锚点的索引const anchorIndex = newEnd + 1;// 锚点元素const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;// 采用 while 循环,调用 patch 函数逐个挂载新增节点while (j <= newEndb) {patch(null, newChildren[j++], container, anchor);}}
}

上面代码中,首先计算锚点的索引值(即 anchorIndex )为 newEnd + 1。如果小于新的一组子节点的数量,则说明锚点元素在新的一组子节点中,所以直接使用 newChildren[anchorIndex].el 作为锚点元素;否则说明索引 newEnd 对应的节点已经是尾部节点了,此时无需提供锚点元素。我们开启一个 while 循环,用来遍历索引 j 和索引 newEnd 之间的节点,并调用 patch 函数挂载他们。

上面的案例展示了新增节点的情况,接下来是删除节点的情况,如下图所示:

在这里插入图片描述

我们同样使用索引 j、oldEnd 和 newEnd 进行标记,如下图所示:
在这里插入图片描述

当对相同的前置节点和后置节点经过预处理后,新的一组子节点已经全部被处理完毕了,而旧的一组子节点中遗留了一个节点p-2。这说明应该卸载节点p-2。实际上遗留的节点可能有多个,如下图所示:

在这里插入图片描述

索引 j 和索引 oldEnd之间的任何节点都应该被卸载,具体实现如下:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 更新相同的前置节点// 省略部分代码// 更新相同的后置节点// 省略部分代码// 预处理完毕后,如果满足如下条件,则说明从 j --> newEnd 之间的节点应作为新节点插入if (j > oldEnd && j <= newEnd) {// 省略部分代码} else if (j > newEnd && j <= oldEnd) {// j -> oldEnd 之间的节点应该被卸载while (j <= oldEnd) {unmount(oldChildren[j++]);}}
}

上面的这段代码中,新增了一个 else…if 分支。当条件满足 j > newEnd && j <= oldEnd 时,则开启一个 while 循环,并调用 unmount 函数逐个进行卸载这些遗留节点。

判断是否需要进行 DOM 移动操作

前面讲了快速 Diff 算法的预处理过程,即处理相同的前置节点和后置节点。但是前面给的例子是比较理想化的,当处理完相同的前置节点和后置节点以后,新旧两组子节点中总会有一组子节点全部都被处理完毕。这种情况下,只需要简单地挂载、卸载节点即可。有时情况会比较复杂,如下图左侧:

在这里插入图片描述

可以看出,与旧的一组子节点相比,新的一组子节点多出了一个新节点p-7,少了一个节点p-6。这个例子中相同地前置节点只有p-1,而相同地后置节点只有p-5,如上图右侧。

经过预处理后地两组子节点状态如下:

在这里插入图片描述

经过预处理后,新旧两组子节点都有部分节点未处理。接下来的处理规则:

  • 判断是否有节点需要移动,以及应该如何移动
  • 找出那些需要被添加或移除地节点

观察上图可知,索引 j 、newEnd 和 oldEnd 不满足下面两个条件中的任何一个:

  • j > oldEnd && j <= newEnd
  • j > newEnd && j <= oldEnd

因此我们需要添加新的分支来处理上图中出现的情况,代码如下。后续处理逻辑会在 else 分支内。

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 更新相同的前置节点// 省略部分代码// 更新相同的后置节点// 省略部分代码// 预处理完毕后,如果满足如下条件,则说明从 j --> newEnd 之间的节点应作为新节点插入if (j > oldEnd && j <= newEnd) {// 省略部分代码} else if (j > newEnd && j <= oldEnd) {// 省略部分代码} else {// 增加 else 分支来处理非理想情况 }
}

接下来地处理思路,首先,我们构造一个数组 source,它的长度等于新的一组子节点经过预处理后剩余节点的数量,并且 source 数组中每个元素的初始值都是 -1,如下图所示:

在这里插入图片描述

通过下面的代码完成 source 数组的构造:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);
}

如上面的代码所示,首先计算新的一组子节点剩余未处理的节点数量,即 newEnd - j + 1,然后创建一个长度与之相同的数组 source ,最后使用 fill 函数完成数组的填充。数组 source 中的每一个元素分别与新的一组子节点剩余未处理的节点对应。实际上, source 数组将用来存储新的一组子节点中的节点在旧的一组子节点中的位置索引,后面将会使用它计算出一个最长递增子序列,并用于辅助完成 DOM 移动的操作。

在这里插入图片描述

上图展示了填充 source 数组的过程,我们可以通过双层 for 循环来完成 source 数组的填充工作,外层循环用于遍历旧的一组子节点,内层循环用来遍历新的一组子节点,代码如下:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// oldStart 和 newStart 分别为起始索引值,即 j const oldStart = j;const newStart = j;// 遍历旧的一组子节点for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];for (let k = newStart; k <= newEnd; k++) {const newVNode = newChildren[k];// 找到拥有相同 key 值的可复用节点if (oldVNode.key === newVNode.key) {// 调用 patch 进行更新patch(oldVNode, newVNode, container);// 最后填充 source 数组source[k - newStart] = i;}}}
}

这里需要注意的是,由于数组 source 的索引是从0开始的,而未处理的节点的索引未必从0开始,所以在填充数组时需要使用表达式 k - newStart 的值作为数组的索引值。外层循环的变量 i 就是当前节点在旧的一组子节点中的位置索引,因此直接将变量 i 的值赋给 source[k - newStart] 即可。

现在 source 数组已经填充完毕,但是代码中我们采用了两层嵌套的循环,其时间复杂度为 O(n1 * n2),其中 n1 和 n2 为新旧两组子节点的数量,我们也可以使用 O(n ^ 2) 表示。当新旧两组子节点数量较多时,双层嵌套循环会带来性能问题。出于优化的目的,我们可以为新的一组子节点构建一张索引表,用来存储节点的 key 和节点位置索引之间的映射,如下图:

在这里插入图片描述

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// oldStart 和 newStart 分别为起始索引值,即 j const oldStart = j;const newStart = j;// 构建索引表const keyIndex = {};for (let i = oldStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 遍历旧的一组子节点中剩余未处理的节点for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 通过索引表快速找到新的一组子节点中具有相同 key 值的节点位置const k = keyIndex[oldVNode.key];if (typeof k !== 'undefined') {const newVNode = newChildren[k];// 调用 patch 进行更新patch(oldVNode, newVNode, container);// 最后填充 source 数组source[k - newStart] = i;} else {// 没找到unmount(oldVNode);}}
}

上面的代码中,同样使用了两个 for 循环,不过降低了时间复杂度至 O(n)。其中第一个 for 循环用于构建索引表,索引表存储的节点的 key 值与节点在新的一组子节点中位置索引之间的映射,第二个 for 循环用于遍历旧的一组子节点。我们拿旧子节点的 key 值去索引表 keyIndex 中查找该节点在新的一组子节点中的位置,并将结果存储到变量 k 中。如果 k 存在,说明节点是可复用的,所以调用 patch 函数进行节点打补丁,并填充 source 数组;否则说明该节点不在新的一组子节点中存在,需要调用 unmount 函数进行卸载。

接下来我们需要判断节点是否需要移动。实际上,快速 Diff 算法判断节点是否需要移动的方法与简单 Diff 算法类似,判断节点是否需要移动的原理:拿新的一组子节点中节点去旧的一组子节点中寻找可复用的节点,如果找到了,则记录该节点的位置,我们这个索引称为最大索引。在整个寻找过程中,如果一个节点的索引值小于最大索引,则说明这个节点对应的真实 DOM 需要移动。如下面代码所示:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// oldStart 和 newStart 分别为起始索引值,即 j const oldStart = j;const newStart = j;// 新增两个变量,moved 和 poslet moved = false;let pos = 0;// 构建索引表const keyIndex = {};for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 遍历旧的一组子节点中剩余未处理的节点for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 通过索引表快速找到新的一组子节点中具有相同 key 值的节点位置const k = keyIndex[oldVNode.key];if (typeof k !== 'undefined') {const newVNode = newChildren[k];// 调用 patch 进行更新patch(oldVNode, newVNode, container);// 最后填充 source 数组source[k - newStart] = i;// 判断节点是否需要移动if (k < pos) {moves = true;} else {pos = k;}} else {// 没找到unmount(oldVNode);}}
}

上面这段代码,我们新增了 pos 和 moved 两个变量。前者初始值为0,代表遍历旧的一组子节点过程中的最大索引值 k;后者初始值是 false,代表是否需要移动节点。在简单Diff 算法时提到,如果在遍历过程中遇到的索引值呈现递增趋势,则说明不许要移动节点,反之则需要。所以在第二个 for 循环内,我们通过比较 k 与 pos 的值来判断节点是否需要移动。

除此之外,我们还需要一个数量标识,代表已经处理过的节点数量已经更新过的节点数量应该小于新的一组子节点中需要更新的节点数量。一旦超过前者,则说明有多余的节点需要被卸载。代码如下:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// oldStart 和 newStart 分别为起始索引值,即 j const oldStart = j;const newStart = j;// 新增两个变量,moved 和 poslet moved = false;let pos = 0;// 新增 patched 变量,代表更新过的节点let patched = 0;// 构建索引表const keyIndex = {};for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 遍历旧的一组子节点中剩余未处理的节点for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 如果更新过的节点数量(patched的值)小于等于需要更新的节点数量(source 数组的长度),则执行更新if (patched <= count) {// 通过索引表快速找到新的一组子节点中具有相同 key 值的节点位置const k = keyIndex[oldVNode.key];if (typeof k !== 'undefined') {const newVNode = newChildren[k];// 调用 patch 进行更新patch(oldVNode, newVNode, container);// 每更新一个节点,都将 patched 变量 +1patched++;// 最后填充 source 数组source[k - newStart] = i;// 判断节点是否需要移动if (k < pos) {moves = true;} else {pos = k;}} else {// 没找到unmount(oldVNode);}} else {// 如果更新过的节点数量(patched的值)大于需要更新的节点数量(source 数组的长度),则卸载多余节点unmount(oldVNode);}}
}

上面的代码中我们增加了 patched 变量,其初始值为0,代表更新过的节点。接着在第二个 for 循环中增加了判断patched <= count,如果此条件成立,则正常执行更新,并且每次更新 patched 的值自增1;否则说明剩下的节点都是多余的,需要调用 unmount 函数进行卸载。

现在我们通过 moved 的值可以判断是否需要移动节点,接下来讨论怎样移动节点。我们知道 source 数组中存储着新的一组子节点中的节点在旧的一组子节点中的位置,后面会根据 source 数组计算出一个最长递增子序列,用于DOM移动操作。代码如下:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 省略部分代码for (let i = oldStart; i <= oldEnd; i++) {// 省略部分代码}if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作}
}

上面的代码中,我们新增了 if 判断分支,如果 moved 为 true,则说明需要进行 DOM 移动操作,所以用于 DOM 移动操作的逻辑将编写在该 if 语句块内。然后我们计算 source 数组的最长递增序列,其中 source 数组仍然取用上面的例子,如下图所示:

在这里插入图片描述

这个例子中,我们计算出 source 数组为 [2,3,1,-1]。

最长递增子序列:**给定一个数值序列,找到它的一个子序列,并且该子序列中的值是递增的,子序列中的元素在原序列不一定是连续。一个序列可能有多个递增子序列,其中最长的那一个就称为最长递增子序列。**举例:假设给定数值序列[0,8,4,12],那么他的最长子系列就是[0,8,12],当然,对于同一个数值序列来说,他的最长递增子序列可能有多个,例如[0,4,12]也是本例的答案之一。

if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作const seq = lis(source); // [0, 1]
}

上面的代码我们使用lis函数计算一个数组的最长递增子序列。lis函数接收 source 数组作为参数,并返回 source 数组的最长递增子序列之一。上面得到 source 数组的最长递增子序列是 [0, 1],因为 lis 函数的返回结果是最长递增子序列中的元素在 source 数组中的位置索引。如下图:
在这里插入图片描述

有了最长递增子序列的索引信息后,下一步要重新对节点进行编号,如下图:

在这里插入图片描述

观察上图,我们忽略了经过预处理的节点p-1和p-5。所以索引为0的节点是p-2,为索引为1的节点是p-3,以此类推。重新编号是为了让子序列 seq 与新的索引值产生对应关系。其实最长递增子序列 seq 拥有一个非常重要的意思。以上例来说,子序列 seq 的值为[0,1],它的含义是:在新的一组子节点中,重新编号后索引值为0和1的这两个节点在更新的前后顺序没有发生变化。换句话说,重新编号后,索引值为0和1的这两个节点不需要移动。在新的一组子节点中,节点p-3的索引为0,节点p-4的索引为1,所以节点p-3和节点p-4所对应的真实 DOM 不需要移动。也就是只有节点p-2和p-7可能需要移动。

为了完成节点的移动,我们还需要创建两个索引值 i 和 s:

  • 用索引 i 指向新的一组子节点中的最后一个节点
  • 用索引 s 指向最长递增子序列中的最后一个元素

如下图:

在这里插入图片描述

观察上图,为了简化图示,我们在去掉了旧的一组子节点以及无关的线条和变量。接下来我们开启一个 for 循环,让变量 i 和 s 按照上图中箭头的方向移动,代码如下:

if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作const seq = lis(source);// s 指向最长递增子序列的最后一个元素let s = seq.length - 1;// i 指向新的一组子节点的最后一个元素let i = count - 1;// for 循环使得 i 递减,即按照图中箭头方向移动for (i; i >= 0; i--) {if (i !== seq[s]) {// 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动} else {// 当 i === seq[s] 时,说明该位置的节点不需要移动// 只需要让 s 指向下一个位置s--;}}
}

上面的代码中,变量 i 就是节点的索引。在 for 循环内,判断条件 i !== seq[s],如果节点的索引 i 不等于 seq[s] 的值,说明该节点对应的真实 DOM 需要移动,否则说明当前访问的节点不需要移动,变量 s 递减。

接下来我们就按照上述思路执行更新。初始时索引 i 指向节点p-7。由于节点p-7对应的 source 数组中相同位置的元素值为 -1,所以我们应该将节点p-7作为全新节点进行挂载,如下面代码所示:

if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作const seq = lis(source);// s 指向最长递增子序列的最后一个元素let s = seq.length - 1;// i 指向新的一组子节点的最后一个元素let i = count - 1;// for 循环使得 i 递减,即按照图中箭头方向移动for (i; i >= 0; i--) {if (source[i] === -1) {// 说明索引 i 的节点是全新的节点,应该将其挂载// 该节点在新的 children 中的真实位置索引const pos = i + newStart;const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;// 锚点const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null;// 挂载patch(null, newVNode, container, anchor);} else if (i !== seq[s]) {// 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动} else {// 当 i === seq[s] 时,说明该位置的节点不需要移动// 只需要让 s 指向下一个位置s--;}}
}

如果 source[i] 的值为-1,说明索引为 i 的节点是全新的节点,于是我们调用 patch 函数将其挂载到容器中。这里需要注意的是,由于索引 i 是重新编号后的,因此为了得到真实索引值,需要计算表达式 i + newStart 的值。

新节点创建完毕后,for 循环已经执行了一次,此时索引 i 向上移动一步,指向了节点p-2,如下图所示:
在这里插入图片描述

接着进行下一轮 for 循环,满足条件 i !== seq[s],此时索引 i 值为2,索引 s 的值为1,故节点p-2需要移动,代码如下:

if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作const seq = lis(source);// s 指向最长递增子序列的最后一个元素let s = seq.length - 1;// i 指向新的一组子节点的最后一个元素let i = count - 1;// for 循环使得 i 递减,即按照图中箭头方向移动for (i; i >= 0; i--) {if (source[i] === -1) {// 省略部分代码} else if (i !== seq[s]) {// 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动// 该节点在新的一组子节点真实的位置索引const pos = i + newStart;const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;// 锚点const anchor = nextPos < newChildren.length ? newChildren[pos].el : null;// 移动insert(newVNode.el, container, anchor);} else {// 当 i === seq[s] 时,说明该位置的节点不需要移动// 只需要让 s 指向下一个位置s--;}}
}

可以看到,移动节点的实现思路类似于挂载全新的节点。不同点在于,移动节点是通过 insert 函数来完成的。接着下一轮的循环。此时索引 i 指向节点p-4,如下图:

在这里插入图片描述

更新过程仍然是分为三个步骤,由于不满足条件 source[i] !== -1 和 i !== seq[s],所以代码会最终执行 else 分支。这意味着,节点p-4所对应的真实 DOM 不需要移动,但仍然需要让索引 s 的值递减,即 s–。

然后进入下一个循环,此时的状态如下图:

在这里插入图片描述

此时索引 i 指向节点p-3。继续判断三个条件,最终得出,代码将执行 else 分支,也就是第三步,意味着节点p-3对应的真实 DOM 也不需要移动,在完成这一轮更新之后,循环将终止,更新完成。

需要强调的是,关于给定序列的递增子序列的求法,只给出下列求解给定序列的最长递增子序列的代码,取自Vue.js3。网络上有大量文章讲解了这方面的内容,可以进行自行查阅。

function getSequence(arr) {const p = arr.slice();const result = [0];let i,j,u,v,c;const len = arr.length;for (i = 0; i < len; i++) {const arrI = arr[i];if (arrI !== 0) {j = result[result.length - 1];if (arr[j] < arrI) {p[i] = j;result.push(i);continue;}u = 0;v = result.length - 1;while(u < v) {c = ((u + v) / 2) | 0;if (arr[result[c]] < arrI) {u = c + 1;} else {v = c;}}if (arrI < arr[result[u]]) {if (u > 0) {p[i] = result[u - 1]}result[u] = i;}}}u = result.length;v = result[u - 1];while(u-- > 0) {result[u] = v;v = p[v];}return result;
}

完整的 patchKeyedChildren 代码如下:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0;let oldVnode = oldChildren[j];let newVNode = newChildren[j];// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container);// 更新索引 j,让其递增j++;oldVnode = oldChildren[j];newVNode = newChildren[j];}// 更新相同的后置节点// 索引 oldEnd 指向旧的一组子节点的最后一个节点let oldEnd = oldChildren.length - 1;// 索引 newEnd 指向新的一组子节点的最后一个节点let newEnd = newChildren.length - 1;oldVnode = oldChildren[oldEnd];newVNode = newChildren[newEnd];// while 循环从后向前遍历,直到遇到拥有不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container);// 递减 oldEnd 和 newEnd --oldEnd;--newEnd;oldVnode = oldChildren[oldEnd];newVNode = newChildren[newEnd];}// 预处理完毕后,如果满足如下条件,则说明从 j --> newEnd 之间的节点应作为新节点插入if (j > oldEnd && j <= newEnd) {// 锚点的索引const anchorIndex = newEnd + 1;// 锚点元素const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;// 采用 while 循环,调用 patch 函数逐个挂载新增节点while (j <= newEndb) {patch(null, newChildren[j++], container, anchor);}} else if (j > newEnd && j <= oldEnd) {// j -> oldEnd 之间的节点应该被卸载while (j <= oldEnd) {unmount(oldChildren[j++]);}} else {// 构造 source 数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// oldStart 和 newStart 分别为起始索引值,即 j const oldStart = j;const newStart = j;// 新增两个变量,moved 和 poslet moved = false;let pos = 0;// 新增 patched 变量,代表更新过的节点let patched = 0;// 构建索引表const keyIndex = {};for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 遍历旧的一组子节点中剩余未处理的节点for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 如果更新过的节点数量小于等于需要更新的节点数量,则执行更新if (patched <= count) {// 通过索引表快速找到新的一组子节点中具有相同 key 值的节点位置const k = keyIndex[oldVNode.key];if (typeof k !== 'undefined') {const newVNode = newChildren[k];// 调用 patch 进行更新patch(oldVNode, newVNode, container);// 每更新一个节点,都将 patched 变量 +1patched++;// 最后填充 source 数组source[k - newStart] = i;// 判断节点是否需要移动if (k < pos) {moves = true;} else {pos = k;}} else {// 没找到unmount(oldVNode);}} else {// 如果更新过的节点数量(patched的值)大于需要更新的节点数量(source 数组的长度),则卸载多余          unmount(oldVNode);}}if (moved) {// 如果 moved 为true,则需要进行 DOM 移动操作const seq = lis(source);// s 指向最长递增子序列的最后一个元素let s = seq.length - 1;// i 指向新的一组子节点的最后一个元素let i = count - 1;// for 循环使得 i 递减,即按照图中箭头方向移动for (i; i >= 0; i--) {if (source[i] === -1) {// 说明索引 i 的节点是全新的节点,应该将其挂载// 该节点在新的 children 中的真实位置索引const pos = i + newStart;const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;// 锚点const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null;// 挂载patch(null, newVNode, container, anchor);} else if (i !== seq[s]) {// 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动// 该节点在新的一组子节点真实的位置索引const pos = i + newStart;const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;// 锚点const anchor = nextPos < newChildren.length ? newChildren[pos].el : null;// 移动insert(newVNode.el, container, anchor);} else {// 当 i === seq[s] 时,说明该位置的节点不需要移动// 只需要让 s 指向下一个位置s--;}}}}
}

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

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

相关文章

AD24-Class、飞线、PCB Nets的管理及添加、层的管理

一、Class 1、Class介绍 2、Class添加与显示 ①添加 ②显示通过Panels-PCB&#xff0c;即可将创建的类显示再左上方窗口 3、Class的编辑管理 ①概述 ②颜色更改 二、飞线 1、概述 2、 飞线的打开、关闭 打开&#xff1a;Alt左上角滑动 N&#xff1a;可以针对性的显示和隐…

深度学习环境配置:Anaconda 安装和 pip 源

conda是一种通用包管理系统&#xff0c;与pip的使用类似&#xff0c;环境管理则允许用户方便地安装不同版本的python并可以快速切换。 Anaconda则是一个打包的集合&#xff0c;里面预装好了conda、某个版本的python、众多packages、科学计算工具等等&#xff0c;就是把很多常用…

Vue3 - 从 vue2 到 vue3 过渡,这一套就够了(案例 + 效果演示)(二)

目录 一、组合式 API 的使用 1.1、watch 函数 1.2、watchEffect 函数 1.3、toRef 和 toRefs 1.3.1、toRef 1.3.2、toRefs 1.4、vue3 的声明周期 一、组合式 API 的使用 1.1、watch 函数 与 vue2.x 中的 watch 配置功能一致&#xff0c;但是多了一些坑&#xff1a; 这…

【蓝桥杯】环形链表的约瑟夫问题

目录 题目描述&#xff1a; 输入描述&#xff1a; 输出描述&#xff1a; 示例1 解法一&#xff08;C&#xff09;&#xff1a; 解法二&#xff08;Cpp&#xff09;&#xff1a; 正文开始&#xff1a; 题目描述&#xff1a; 据说著名犹太历史学家 Josephus 有过以下故事&a…

Pytest测试用例参数化

pytest.mark.parametrize(参数名1,参数名2...参数n, [(参数名1_data1,参数名2_data1...参数名n_data1),(参数名1_data2,参数名2_data2...参数名n_data2)]) 场景&#xff1a; 定义一个登录函数test_login,传入参数为name,password&#xff0c;需要用多个账号去测试登录功能 # …

JAVA方法引用:

方法引用的出现原因在使用Lambda表达式的时候&#xff0c;我们实际上传递进去的代码就是一种解决方案&#xff1a;拿参数做操作那么考虑一种情况&#xff1a;如果我们在Lambda中所指定的操作方案&#xff0c;已经有地方存在相同方案&#xff0c;那是否还有必要再写重复逻辑呢&a…

Java SWT Composite 绘画

Java SWT Composite 绘画 1 Java SWT2 Java 图形框架 AWT、Swing、SWT、JavaFX2.1 Java AWT (Abstract Window Toolkit)2.2 Java Swing2.3 Java SWT (Standard Widget Toolkit)2.4 Java JavaFX 3 比较和总结 1 Java SWT Java SWT&#xff08;Standard Widget Toolkit&#xff…

02 使用jdk运行第一个java程序:HelloWorld

使用jdk运行第一个java程序 1 HelloWorld小案例1.1 编写流程1.2 错误示例 首先在CMD命令行里面&#xff0c;使用javac xxxx.java&#xff0c; 进行编译&#xff0c;其中会有报错&#xff1b; 然后生成xxxx.class 文件&#xff0c;然后使用java xxxx.class 进行运行。 1 HelloWo…

瑞_23种设计模式_抽象工厂模式

文章目录 1 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;1.1 概念1.2 介绍1.3 小结1.4 结构 2 案例一2.1 案例需求2.2 代码实现 3 案例二3.1 需求3.2 实现 4 总结4.1 抽象工厂模式优缺点4.2 抽象工厂模式使用场景4.3 抽象工厂模式VS工厂方法模式4.4 抽象工厂…

node.js基础--01

Author nodes&#xff1a;&#xff08;题记&#xff09; node.js is an open-source&#xff0c;cross-platform JAVAScript runtime environment。 node.js是一个开源&#xff0c;跨平台的js运行环境 common commands&#xff08;常用指令&#xff09; 1、C: enter hard …

javaScript的序列化与反序列化

render函数的基本实现 javaScript的序列化与反序列化 一&#xff0c;js中的序列化二&#xff0c;序列化三&#xff0c;反序列化四&#xff0c;总结 一&#xff0c;js中的序列化 js中序列化就是对象转换成json格式的字符串&#xff0c;使用JSON对象的stringify方法&#xff0c;…

新书速览|Python数据科学应用从入门到精通

系统教授数据科学与Python实战&#xff0c;涵盖线性回归、逻辑回归、决策树、随机森林、神经网 本书内容 随着数据存储、数据处理等大数据技术的快速发展&#xff0c;数据科学在各行各业得到广泛的应用。数据清洗、特征工程、数据可视化、数据挖掘与建模等已成为高校师生和职场…

STL——空间配置器

空间配置器是STL六大组件之一&#xff0c;它和其他五个组件相互配合&#xff0c;起着很关键的作用。 容器&#xff1a;各种数据结构、如vector、list、stack、deque、queue、set、map、unordered_map等等算法&#xff1a;各种算法&#xff0c;如sort、serach、copy、erase 提供…

C#用正则表达式验证格式:电话号码、密码、邮编、手机号码、身份证、指定的小数点后位数、有效月、有效日

正则表达式在程序设计中有着重要的位置&#xff0c;经常被用于处理字符串信息。 用Regex类的IsMatch方法&#xff0c;使用正则表达式可以验证电话号码是否合法。 一、涉及到的知识点 Regex类的IsMatch方法用于指示正则表达式使用pattern参数中指定的正则表达式是否在输入字符串…

计算机网络_1.6.2 计算机网络体系结构分层的必要性

1.6.2 计算机网络体系结构分层的必要性 一、五层原理体系结构每层各自主要解决什么问题1、物理层2、数据链路层3、网络层4、运输层5、应用层 二、总结三、练习 笔记来源&#xff1a; B站 《深入浅出计算机网络》课程 本节主要介绍实现计算机网络需要解决哪些问题&#xff1f;以…

这种学习单片机的顺序是否合理?

这种学习单片机的顺序是否合理&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01…

随着网络的快速发展,网络安全问题也日益凸显,遇到攻击该如何处理,如何抉择合适的防护方案

DexunCloud 经过研究发现当今世界&#xff0c;随着网络的快速发展&#xff0c;网络安全问题也日益凸显。其中&#xff0c;DDoS&#xff08;分布式拒绝服务&#xff09;攻击被认为是网络安全领域里最为严重的威胁之一。毫无疑问&#xff0c;DDoS攻击不仅可以导致网络服务中断&am…

支付宝直连商户处理支付交易投诉管理,支持多商户

大家好&#xff0c;我是小悟 1、问题背景 玩过支付宝生态的&#xff0c;或许就有这种感受&#xff0c;如果收到投诉单&#xff0c;不会通知到手机端&#xff0c;只会在支付宝商家后台-账号中心-安全中心-消费者投诉-支付交易投诉那里显示。那你能一直盯着电脑看吗&#xff1f;…

Flink CEP(基本概念)

Flink CEP 在Flink的学习过程中&#xff0c;我们已经掌握了从基本原理和核心层的DataStream API到底层的处理函数&#xff0c;再到应用层的Table API和SQL的各种手段&#xff0c;可以应对实际应用开发的各种需求。然而&#xff0c;在实际应用中&#xff0c;还有一类更为复…

AI Partition(银灿U盘分区工具)V2.0.0.3

AI Partition(银灿U盘分区工具)V2.0.0.3.zip 复制链接下载吧 https://url20.ctfile.com/f/36743220-1017367709-67f1b9?p2024 (访问密码: 2024) 支持IS903B IS902E IS916 AI Partition(银灿U盘分区量产工具) 这个是银灿官方发布的最新版U盘分区工具&#xff0c;版本号V2.0.0…