《Vuejs设计与实现》第 11 章(快速 diff 算法

目录

11.1 相同的前置元素和后置元素

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

11.3 如何移动元素

11.4 总结


我们将探讨第三种用于比较新旧子节点集合的方法:快速Diff算法。
这种算法的速度非常快,最早应用于 ivi 和 inferno 框架,DOM 操作方面的性能略优于 Vue.js 2 的双端 Diff 算法,Vue3 也对其进行了借鉴和扩展。

11.1 相同的前置元素和后置元素

快速 Diff 算法包含预处理步骤,这实际上是借鉴了纯文本 Diff 算法的思路。在纯文本 Diff 算法中,需要对两段文本进行预处理。
例如,在进行 Diff 之前,可以先进行全等比较:

if (text1 === text2) return

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

I use vue for app development
I use react for app development

上面两段文本的头部和尾部分别有一段相同的内容,对于内容相同的问题,是不需要进行核心 Diff 操作的。因此,上面两句话,真正需要进行 Diff 操作的部分是:

vue
react

这实际上是简化问题的一种方式。这样的好处是在特定情况下我们能够轻松地判断文本的插入和删除:

I like you
I like you too

经过预处理,去掉这两段文本中相同的前缀内容和后缀内容之后,它将变成:


too

可以看到,经过预处理后,第一段的内容为空。这说明第二段的内容在第一段的基础上增加了字符串 too。

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

image.png


观察发现,两组子节点具有相同的前置节点 p-1,以及相同的后置节点 p-2 和 p-3
对于相同的前置节点和后置节点,因为它们在新旧子节点组中的相对位置不变,我们无须移动它们,但仍需在它们之间打补丁。
对于前置节点,我们可以建立索引 j,其初始值为 0,用于指向两组子节点的开头:
 

image.png


然后开启一个 while 循环,让索引 j 递增,直到遇到不相同的节点为止,如下代码所示:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.childrenconst oldChildren = n1.children// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0let 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 循环终止时,索引 j 的值为 1。
这样,我们就完成了对前置节点的更新,如图所示:

image.png


接下来,我们需要处理相同的后置节点。
由于新旧两组子节点的数量可能不同,所以我们需要两个索引 newEnd 和 oldEnd,分别指向新旧两组子节点的最后一个节点,如图所示:
 

image.png

然后,再开启一个 while 循环,并从后向前遍历这两组子节点,直到遇到 key 值不同的节点为止: 

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

处理后置节点的方式与处理前置节点一样,在 while 循环内,需要调用 patch 函数进行打补丁,然后递减两个索引 oldEnd、newEnd 的值,处理后如图所示:
 

image.png


上图显示,处理完相同的前置和后置节点后,旧子节点组已全部处理,而新子节点组仍有一个未处理的节点 p-4。
实际上,节点 p-4 是新增节点。怎么得出节点 p-4 是新增节点结论的呢?我们需要观察索引 j、newEnd 和 oldEnd 之间的关系:

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

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

image.png


在新子节点组中,需要将索引值处于 j 和 newEnd 之间的所有节点作为新子节点挂载。
为了将这些节点挂载到正确的位置,我们需要找到正确的锚点元素。
观察上图我们可以知道新增节点应该挂载在节点 p-2 对应的真实 DOM 前面。
因此,节点 p-2 对应的真实 DOM 节点就是锚点元素:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.childrenconst 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 <= newEnd) {patch(null, newChildren[j++], container, anchor)}}
}

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

我们接下来看看删除节点的情况:
 

image.png


我们同样使用索引 j、oldEnd 和 newEnd 进行标记,如图所示:
 

image.png


接着,对相同的前置节点进行预处理,处理后的状态如图:
 

image.png


然后,对相同的后置节点进行预处理,处理后的状态如图:
 

image.png


当前置和后置节点全处理完了,还遗留一个节点 p-2,这说明,应该卸载节点 p-2。实际情况可能会有多个遗留元素,如图:
 

image.png


我们需要卸载索引 j 和 oldEnd 之间的所有节点,具体实现如下:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.childrenconst oldChildren = n1.children// 更新相同的前置节点// 省略部分代码// 更新相同的后置节点// 省略部分代码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 函数,逐个卸载这些遗留节点。

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

上一节中,新旧两组子节点中总会有一组子节点全部被处理完毕。后续只需要简单的挂载和卸载节点即可,我们来看一下更复杂的情况:

image.png


新节点组增加了一个节点 p-7,同时减少了一个节点 p-6。
在这个例子中,只有 p-1 是相同的前置节点,而 p-5 是相同的后置节点。
经过预处理后两组子节点的状态:
 

image.png


可以看出,新旧节点组都有部分未处理节点。此时,我们需要进一步处理。
所有的 Diff 算法,无论是简单的,双端的,还是本章介绍的快速 Diff 算法,都遵循同样的规则:判断是否有节点需要移动,以及应该如何移动,找出需要被添加或移除的节点。
接下来我们的任务就是确定哪些节点需要移动,以及如何移动。在这种非理想的情况下,处理完相同的前置和后置节点后,索引 j、newEnd 和 oldEnd 不满足以下任何一个条件:

  1. j > oldEnd && j <= newEnd
  2. j > newEnd && j <= oldEnd

因此,我们需要添加一个新的else分支来处理这种情况:

function patchKeyedChildren(n1, n2, container) {const newChildren = n2.childrenconst oldChildren = n1.children// 更新相同的前置节点和后置节点,代码省略if (j > oldEnd && j <= newEnd) {// 省略部分代码} else if (j > newEnd && j <= oldEnd) {// 省略部分代码} else {// 处理非理想情况的新else分支}
}

接下来我们将在这个 else 分支内编写后续的处理逻辑。
首先,我们需要构造一个数组 source,长度等于新节点组在预处理后剩余未处理节点的数量,每个元素的初始值都是 -1:
 

image.png


我们可以通过下面的代码完成 source 数组的构造:

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

source 数组后续将存储新节点组中的节点在旧节点组中的位置索引,后面我们将使用它计算出一个最长递增子序列,并用于协助完成 DOM 移动的操作。
 

image.png


上图展示了填充 source 数组的过程,由于 source 数组存储的是新子节点在旧的一组子节点中的位置索引,所以有:

  • 新的一组子节点中的节点 p-3 在旧的一组子节点中的索引为 2, 因此 source 数组的第一个元素值为 2。
  • 新的一组子节点中的节点 p-4 在旧的一组子节点中的索引为 3, 因此 source 数组的第二个元素值为 3。
  • 新的一组子节点中的节点 p-2 在旧的一组子节点中的索引为 1, 因此 source 数组的第三个元素值为 1。
  • 新的一组子节点中的节点 p-7 比较特殊,因为在旧的一组子节点中没有与其 key 值相等的节点,所以 source 数组的第四个元素 值保留原来的 -1。

我们可以通过两层 for 循环来完成 source 数组的填充,外层循环遍历旧节点组,内层循环遍历新节点组:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {const count = newEnd - j + 1const source = new Array(count)source.fill(-1)// oldStart 和 newStart 分别为起始索引,即 jconst oldStart = jconst 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(n^2),n1 和 n2 分别代表新旧两组子节点的数量。
当新旧两组子节点的数量较多时,这种方法可能会导致性能问题。
为了优化性能,我们可以创建一个索引表来映射新子节点的 key 和位置索引,这样可以避免内嵌循环,降低时间复杂度至O(n)。

image.png

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

 上述代码,我们首先建立了一个索引表 keyIndex,用来存储节点的 key 和节点位置索引之间的映射。
然后在第一个 for 循环中遍历新的子节点并填充索引表。在第二个 for 循环中,我们遍历旧的子节点,并使用索引表快速找到新子节点中对应 key 的节点位置 k,如果 k 存在,就进行 patch 操作并填充 source 数组;如果 k 不存在,就卸载旧的节点。
此外,我们还需要判断节点是否需要移动。快速 Diff 算法判断节点是否需要移动的方法与简单 Diff 算法类似:

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)const oldStart = jconst newStart = j// 新增两个变量,moved 和 poslet moved = falselet pos = 0const keyIndex = {}for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i}for (let i = oldStart; i <= oldEnd; i++) {oldVNode = oldChildren[i]const k = keyIndex[oldVNode.key]if (typeof k !== 'undefined') {newVNode = newChildren[k]patch(oldVNode, newVNode, container)source[k - newStart] = i// 判断节点是否需要移动if (k < pos) {moved = true} else {pos = k}} else {unmount(oldVNode)}}
}

上述代码,我们新增了两个变量 moved 和 pos。前者的初始值为 false,代表是否需要移动节点,后者的初始值为 0,代表遍历旧的一组子节点的过程中遇到的最大索引值 k。
如果在遍历过程中遇到的索引值呈现递增趋势,则说明不需要移动节点,反之则需要。所以在第二个for 循环内,我们通过比较变量 k 与变量 pos 的值来判断是否需要移动节点。
除此之外,我们还需要一个数量标识,代表已经更新过的节点数量。
我们知道,已经更新过的节点数量应该小于新的一组子节点中需要更新的节点数量。一旦前者超过后者,则说明有多余的节点,我们应该将它们卸载,如下面的代码所示:

if (j > oldEnd && j <= newEnd) {// 省略部分代码
} else if (j > newEnd && j <= oldEnd) {// 省略部分代码
} else {// 构造 source 数组const count = newEnd - j + 1const source = new Array(count)source.fill(-1)const oldStart = jconst newStart = jlet moved = falselet pos = 0const keyIndex = {}for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i}// 新增 patched 变量,代表更新过的节点数量let patched = 0for (let i = oldStart; i <= oldEnd; i++) {oldVNode = oldChildren[i]// 如果更新过的节点数量小于等于需要更新的节点数量,则执行更新if (patched <= count) {const k = keyIndex[oldVNode.key]if (typeof k !== 'undefined') {newVNode = newChildren[k]patch(oldVNode, newVNode, container)// 每更新一个节点,都将 patched 变量 +1patched++source[k - newStart] = iif (k < pos) {moved = true} else {pos = k}} else {// 没找到unmount(oldVNode)}} else {// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余的节点unmount(oldVNode)}}
}

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

11.3 如何移动元素

在上一节,我们实现了两个关键步骤:判断是否需要进行 DOM 移动操作,以及构建 source 数组。
当变量 moved 为真时,表示需要进行 DOM 移动操作。source 数组则用于存储新子节点在旧子节点中的位置,这会帮助我们计算出一个最长递增子序列,以便进行DOM移动操作。
以下代码示例展示了如何进行这种移动:

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

在这段代码中,我们在 for 循环后增加了一个if条件判断。如果 moved 为真,则表示需要进行 DOM 移动操作,我们将在此 if 语句编写相关移动的逻辑。
要进行 DOM 移动操作,我们首先需要计算 source 数组的最长递增子序列。
我们先要搞清楚什么是一个序列的递增子序列。
子序列中的元素在原序列中不一定连续。一个序列可能有很多个递增子序列,其中最长的那一个就称为最长递增子序列。
举个例子,假设给定数值序列 [ 0, 8, 4, 12 ],那么它的最长递增子序列就是 [0, 8,12]。当然,对于同一个数值序列来说,它的最长递增子序列可能有多个,例如 [0,4, 12] 也是本例的答案之一。
例如,假设 source 数组为 [2, 3, 1, -1]。如图所示:
 

image.png


这种情况下,最长递增子序列是 [2, 3]。然后,我们可以用如下的代码来求解这个子序列:

if (moved) {// 计算最长递增子序列const seq = lis(sources) // [ 0, 1 ]
}

这段代码中,我们使用了 lis 函数来计算最长递增子序列。这个函数接收 source 数组作为参数,并返回其中的最长递增子序列之一。值得注意的是,返回结果是最长递增子序列元素在 source 数组中的索引,所以没有返回 [2, 3],。如图所示:

image.png


有了最长递增子序列的索引信息后,我们可以开始重新对节点进行编号,如图所示:

image.png

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

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

image.png


接下来,我们将开启一个 for 循环,让变量 i 和 s 按照上图中箭头的方向移动,如下面的代码所示:

if (moved) {const seq = lis(sources)// 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--}}
}

上述代码,for 循环用于逐个访问新的节点组的节点,在 for 循环内,判断条件 i !== seq[s],如果节点的索引 i 不等于 seq[s] 的值,则说明该节点对应的真实 DOM 需要移动,否则说明当前访问的节点不需要移动。
按照上述思路执行更新。初始时索引 i 指向节点 p-7。由于节点 p-7对应的 source 数组中相同位置的元素值为 -1,所以我们应该将节点 p-7 作为全新的节点进行挂载,如下面的代码所示:

if (moved) {const seq = lis(sources)// s 指向最长递增子序列的最后一个元素let s = seq.length - 1// i 指向新的一组子节点的最后一个元素let i = count - 1// for 循环使得 i 递减,即按照图 11-24 中箭头的方向移动for (i; i >= 0; i--) {if (source[i] === -1) {// 说明索引为 i 的节点是全新的节点,应该将其挂载// 该节点在新 children 中的真实位置索引const pos = i + newStartconst 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,如图所示:
 

image.png


接着,进行下一轮 for 循环,步骤如下:

  1. source[i] 是否等于 -1?很明显,此时索引 i 的值为 2,source[2] 的值等于 1,因此节点 p-2 不是全新的节点,不需要挂载它,进行下一步的判断。
  2. i !== seq[s] 是否成立?此时索引 i 的值为 2,索引 s 的值为 1。因此 2!== seq[1] 成立,节点 p-2 所对应的真实 DOM 需要移动。

我们知道了节点 p-2 所对应的真实 DOM 应该移动。实现代码如下:

if (moved) {const seq = lis(sources)// s 指向最长递增子序列的最后一个元素let s = seq.length - 1let i = count - 1for (i; i >= 0; i--) {if (source[i] === -1) {// 省略部分代码} else if (i !== seq[s]) {// 说明该节点需要移动// 该节点在新的一组子节点中的真实位置索引const pos = i + newStartconst newVNode = newChildren[pos]// 该节点的下一个节点的位置索引const nextPos = pos + 1// 锚点const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null// 移动insert(newVNode.el, container, anchor)} else {// 当 i === seq[s] 时,说明该位置的节点不需要移动// 并让 s 指向下一个位置s--}}
}

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

image.png


更新过程仍然分为三个步骤:

  1. 判断表达式 source[i] 的值是否等于 -1?很明显,此时索引 i 的值为1,表达式 source[1] 的值等于 3,条件不成立。所以节点 p-4 不是全新的节点,不需要挂载它。接着进行下一步判断。
  2. 判断表达式 i !== seq[s] 是否成立?此时索引 i 的值为 1,索引 s 的值为 1。这时表达式 1 === seq[1] 为真,所以条件 i !== seq[s] 也不成立。
  3. 由于第一步和第二步中的条件都不成立,所以代码会执行最终的 else 分支。这意味着,节点 p-4 所对应的真实 DOM 不需要移动,但我们仍然需要让索引 s 的值递减,即 s--。

经过三步判断之后,我们得出结论:节点 p-4 不需要移动。于是进行下一轮循环,此时的状态如图:
 

image.png


此时索引 i 指向节点 p-3。我们继续进行三个步骤的判断:

  1. 判断表达式 source[i] 的值是否等于 -1?很明显,此时索引 i 的值为 0,表达式 source[0] 的值等于 2,所以节点 p-3 不是全新的节点,不需要挂载它,接着进行下一步判断。
  2. 判断表达式 i !== seq[s] 是否成立?此时索引 i 的值为 0,索引 s 的值也为 0。这时表达式 0 === seq[0] 为真,因此条件也不成立,最终将执行 else 分支的代码,也就是第三步。
  3. 到了这里,意味着节点 p-3 所对应的真实 DOM 也不需要移动。

在这一轮更新完成之后,循环将会停止,更新完成。

如下是用于求解给定序列的最长递增子序列的代码,取自 Vue.js 3:

function getSequence(arr) {const p = arr.slice()const result = [0]let i, j, u, v, cconst len = arr.lengthfor (i = 0; i < len; i++) {const arrI = arr[i]if (arrI !== 0) {j = result[result.length - 1]if (arr[j] < arrI) {p[i] = jresult.push(i)continue}u = 0v = result.length - 1while (u < v) {c = ((u + v) / 2) | 0if (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.lengthv = result[u - 1]while (u-- > 0) {result[u] = vv = p[v]}return result
}

11.4 总结

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

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

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

相关文章

JavaScript 存储对象 sessionStorage (会话存储) 和 localStorage(本地存储)

深入理解 localStorage localStorage 是浏览器提供的一种客户端存储机制&#xff0c;用于在用户浏览器中存储键值对数据。与 cookie 相比&#xff0c;它提供了更大的存储容量&#xff08;通常为 5-10MB&#xff09;&#xff0c;并且不会随 HTTP 请求发送到服务器&#xff0c;因…

Z-Ant开源程序是简化了微处理器上神经网络的部署和优化

​一、软件介绍 文末提供程序和源码下载 Z-Ant &#xff08;Zig-Ant&#xff09; 是一个全面的开源神经网络框架&#xff0c;专门用于在微控制器和边缘设备上部署优化的 AI 模型。Z-Ant 使用 Zig 构建&#xff0c;为资源受限的硬件上的模型优化、代码生成和实时推理提供端到端…

Linux系统---Nginx配置nginx状态统计

配置Nignx状态统计 1、下载vts模块 https://github.com/vozlt/nginx-module-vts [rootclient ~]# nginx -s stop [rootclient ~]# ls anaconda-ks.cfg nginx-1.27.3 ceph-release-1-1.el7.noarch.rpm nginx-1.27.3.tar.gz info.sh …

深入理解 C++ Lambda表达式:四大语法特性 + 六大高频考点全解析

Lambda表达式是C11引入的一项重要特性&#xff0c;它极大地改变了我们编写匿名函数的方式。 一、为什么会有Lambda表达式 在C11之前&#xff0c;当我们需要传递一个简单的函数时&#xff0c;通常有以下几种选择&#xff1a; 1.1、定义一个单独的函数 // 单独定义的比较函数…

SpringBoot 自动化部署实战:CI/CD 整合方案与避坑全指南

在数字化转型浪潮席卷全球的当下&#xff0c;企业对软件交付的速度与质量提出了前所未有的高要求。SpringBoot 凭借其 “约定优于配置” 的特性&#xff0c;成为 Java 领域快速构建应用的热门框架。而将 SpringBoot 与 CI/CD&#xff08;持续集成 / 持续交付&#xff09;相结合…

JVM字节码文件结构深度剖析

反汇编&#xff0c;以下命令可以查看相对可读的详细结构 javap -verbose ByteCode.class与Class二进制文件并不是直接对齐的 Class二进制文件结构参照表 ClassFile {u4 magic;魔数u2 minor_version;副版本号u2 major_version;主版本号u2…

跟着chrome面板优化页面性能

没有优化前&#xff1a; 1.对文本进行压缩&#xff1a; 重新打包 运行 评分好像还是没有发生改变&#xff0c;于是我去找别的压缩的途径&#xff0c; npm install --save-dev vite-plugin-compression 然后修改vite.config.js文件 导入compression插件 文件夹中也成功出现了…

网上花店微信小程序完整项目

概述 一款功能完善的网上花店微信小程序完整项目。该项目包含了完整的前后端代码&#xff0c;是一款基于Java技术栈开发的电商类小程序&#xff0c;适合初学者学习的小程序源码。 主要内容 该花店小程序源码采用主流技术架构开发&#xff0c;主要功能模块包括&#xff1a; …

Elasticsearch 搜索的流程

Elasticsearch 的搜索流程是一个分布式协作过程&#xff0c;主要包含 ‌查询阶段&#xff08;Query Phase&#xff09;‌ 和 ‌取回阶段&#xff08;Fetch Phase&#xff09;‌&#xff0c;默认采用 QUERY_THEN_FETCH 模式。以下是详细流程&#xff1a; 一、请求分发与路由 ‌…

用户行为分析:从概念到实践的全面指南

在数字化转型浪潮中&#xff0c;用户行为分析已成为企业决策的核心驱动力。 用户行为分析本质上是对用户与产品交互过程中产生的各类行为数据进行系统性收集、处理和分析&#xff0c;从而揭示用户偏好、预测行为趋势并指导业务决策的过程。它包含三层核心要素&#xff1a;行为…

Claude Code - 终端智能编码助手

文章目录 一、关于 Claude Code1、项目概览2、相关链接资源 二、安装配置三、使用指南1、快速开始2、问题反馈 四、隐私与数据1、数据收集2、隐私保护 一、关于 Claude Code 1、项目概览 Claude Code 是一款终端智能编码工具&#xff0c;能够理解代码库并通过自然语言命令执行…

如何在FastAPI中玩转跨服务权限校验的魔法?

title: 如何在FastAPI中玩转跨服务权限校验的魔法? date: 2025/06/24 08:23:40 updated: 2025/06/24 08:23:40 author: cmdragon excerpt: FastAPI跨服务权限校验通过可信令牌颁发、令牌传播机制和分布式验证实现微服务架构安全。核心组件包括令牌生成服务和验证逻辑,使用…

用 Python 打造立体数据世界:3D 堆叠条形图绘制全解析

在数据可视化的工具箱里&#xff0c;3D 图表总能带来眼前一亮的效果 —— 它突破了二维平面的限制&#xff0c;用立体空间展示多维度数据关系&#xff0c;让复杂的数据层级一目了然。今天我们要解锁的「3D 堆叠条形图」&#xff0c;就是一种能同时呈现类别、子类别、数值大小的…

互联网大厂Java求职面试:AI与大模型技术下的RAG系统架构设计与性能优化

【互联网大厂Java求职面试&#xff1a;AI与大模型技术下的RAG系统架构设计与性能优化】 文章内容 面试官开场白 技术总监&#xff08;李明&#xff09;&#xff1a; “郑薪苦&#xff0c;欢迎来到今天的面试。我是李明&#xff0c;负责我们公司的AI平台架构设计。今天我们将围…

kotlin, BigDecimal可以直接使用大于号>、小于号<进行直接比较大小吗

kotlin&#xff0c; BigDecimal可以直接使用大于号>、小于号<进行直接比较大小吗&#xff0c;比如 if (BigDecimal(count) < BigDecimal(100) &#xff09; deepseek回答&#xff1a; 我们正在讨论Kotlin中的BigDecimal比较操作。 用户的问题&#xff1a;是否可以直接…

Harmony状态管理AppStorageV2和PersistenceV2

深入理解ArkUI中的AppStorageV2与PersistenceV2装饰器 引言 在ArkUI应用开发中&#xff0c;状态管理是构建复杂应用的关键环节。随着ArkUI状态管理V2版本的推出&#xff0c;AppStorageV2和PersistenceV2装饰器为开发者提供了更强大、更灵活的状态管理能力。本文将详细介绍这两…

LayUI的table实现行上传图片+mvc

一、layUIJQuery using AMes.Domain.Entity.SystemManage; {Layout null; }<!DOCTYPE html><html> <head><meta name"viewport" content"widthdevice-width" /><title>不合格品处置申请</title><link href"…

ALINX 国产化 FPGA SoM 核心板选型指南:紫光同创 Kosmo2/Titan2/ Logos2/Logos 深度解析

作为紫光同创官方合作伙伴&#xff0c;ALINX 近日发布基于 Kosmo-2 系列新品 PG2K100 核心板 K100。 35mm42mm 的精小尺寸中集成双核 A53 处理器85K FPGA 逻辑单元&#xff0c;1GB DDR3 保障实时数据处理能力&#xff0c;120 pin 工业连接器直插各类设备底板&#xff0c;为空间…

从零到一构建一个现代“C++游戏自研引擎”开发蓝图

当然不可能是真从零到一了&#xff0c;做为一个标题党&#xff0c;标题不牛对不起自己&#xff0c;因为游戏引擎涉及太多领域了&#xff0c;比如图形渲染、物理模拟、音频处理、网络通信等等。每个领域都有专业的解决方案&#xff0c;自己从头实现不仅效率低&#xff0c;而且质…

XSS-labs靶场实战

本文主要对XSS-labs靶场进行介绍&#xff0c;给出了我一步步怎么发现漏洞的过程。 目录 第一关 第二关 第三关 第四关 第五关 第六关 第七关 第八关 第九关 第十关 第十一关 第十二关 第十三关 第十四关 第十五关 第十六关 第一关 没啥好说的&#xff0c;直接…