当新旧子节点类型都为Array_Children类型时,就会触发diff算法,该方法内部主要被分为了五个场景。
1.自前向后对比
2.自后向前对比
3.新节点多余旧节点,需要挂载
4.旧节点多余新节点,需要卸载
5.乱序
自前向后diff,自后向前diff,新节点多于旧节点,旧节点多于新节点
/**
* diff
*/
const patchKeyedChildren = (oldChildren, newChildren, container, parentAnchor) => {
/**
* 索引
*/
let i = 0
/**
* 新的子节点的长度
*/
const newChildrenLength = newChildren.length
/**
* 旧的子节点最大(最后一个)下标
*/
let oldChildrenEnd = oldChildren.length - 1
/**
* 新的子节点最大(最后一个)下标
*/
let newChildrenEnd = newChildrenLength - 1
// 1. 自前向后的 diff 对比。经过该循环之后,从前开始的相同 vnode 将被处理
while (i <= oldChildrenEnd && i <= newChildrenEnd) {
const oldVNode = oldChildren[i]
const newVNode = normalizeVNode(newChildren[i])
// 如果 oldVNode 和 newVNode 被认为是同一个 vnode,则直接 patch 即可
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode, container, null)
}
// 如果不被认为是同一个 vnode,则直接跳出循环
else {
break
}
// 下标自增
i++
}
}
// 2. 自后向前的 diff 对比。经过该循环之后,从后开始的相同 vnode 将被处理
while (i <= oldChildrenEnd && i <= newChildrenEnd) {
const oldVNode = oldChildren[oldChildrenEnd]
const newVNode = normalizeVNode(newChildren[newChildrenEnd])
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode, container, null)
} else {
break
}
oldChildrenEnd--
newChildrenEnd--
}
// 3. 新节点多余旧节点时的 diff 比对。
if (i > oldChildrenEnd) {
if (i <= newChildrenEnd) {
const nextPos = newChildrenEnd + 1
const anchor =
nextPos < newChildrenLength ? newChildren[nextPos].el : parentAnchor
while (i <= newChildrenEnd) {
patch(null, normalizeVNode(newChildren[i]), container, anchor)
i++
}
}
}
// 4. 旧节点多与新节点时的 diff 比对。
else if (i > newChildrenEnd) {
while (i <= oldChildrenEnd) {
unmount(oldChildren[i])
i++
}
}
当新旧节点是同类型节点时,直接patch,否则跳出循环。自前向后的对比和自后向前的对比都是这样。当旧节点多与新节点时,需要判断一次是向前插入还是向后插入,然后在patch。当旧节点多余新节点时,卸载多的旧节点。
乱序diff
最长增长子序列
function getSequence(arr) {
// 获取一个数组浅拷贝。注意 p 的元素改变并不会影响 arr
// p 是一个最终的回溯数组,它会在最终的 result 回溯中被使用
// 它会在每次 result 发生变化时,记录 result 更新前最后一个索引的值
const p = arr.slice()
// 定义返回值(最长递增子序列下标),因为下标从 0 开始,所以它的初始值为 0
const result = [0]
let i, j, u, v, c
// 当前数组的长度
const len = arr.length
// 对数组中所有的元素进行 for 循环处理,i = 下标
for (i = 0; i < len; i++) {
// 根据下标获取当前对应元素
const arrI = arr[i]
//
if (arrI !== 0) {
// 获取 result 中的最后一个元素,即:当前 result 中保存的最大值的下标
j = result[result.length - 1]
// arr[j] = 当前 result 中所保存的最大值
// arrI = 当前值
// 如果 arr[j] < arrI 。那么就证明,当前存在更大的序列,那么该下标就需要被放入到 result 的最后位置
if (arr[j] < arrI) {
p[i] = j
// 把当前的下标 i 放入到 result 的最后位置
result.push(i)
continue
}
// 不满足 arr[j] < arrI 的条件,就证明目前 result 中的最后位置保存着更大的数值的下标。
// 但是这个下标并不一定是一个递增的序列,比如: [1, 3] 和 [1, 2]
// 所以我们还需要确定当前的序列是递增的。
// 计算方式就是通过:二分查找来进行的
// 初始下标
u = 0
// 最终下标
v = result.length - 1
// 只有初始下标 < 最终下标时才需要计算
while (u < v) {
// (u + v) 转化为 32 位 2 进制,右移 1 位 === 取中间位置(向下取整)例如:8 >> 1 = 4; 9 >> 1 = 4; 5 >> 1 = 2
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Right_shift
// c 表示中间位。即:初始下标 + 最终下标 / 2 (向下取整)
c = (u + v) >> 1
// 从 result 中根据 c(中间位),取出中间位的下标。
// 然后利用中间位的下标,从 arr 中取出对应的值。
// 即:arr[result[c]] = result 中间位的值
// 如果:result 中间位的值 < arrI,则 u(初始下标)= 中间位 + 1。即:从中间向右移动一位,作为初始下标。 (下次直接从中间开始,往后计算即可)
if (arr[result[c]] < arrI) {
u = c + 1
} else {
// 否则,则 v(最终下标) = 中间位。即:下次直接从 0 开始,计算到中间位置 即可。
v = c
}
}
// 最终,经过 while 的二分运算可以计算出:目标下标位 u
// 利用 u 从 result 中获取下标,然后拿到 arr 中对应的值:arr[result[u]]
// 如果:arr[result[u]] > arrI 的,则证明当前 result 中存在的下标 《不是》 递增序列,则需要进行替换
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
// 进行替换,替换为递增序列
result[u] = i
}
}
}
// 重新定义 u。此时:u = result 的长度
u = result.length
// 重新定义 v。此时 v = result 的最后一个元素
v = result[u - 1]
// 自后向前处理 result,利用 p 中所保存的索引值,进行最后的一次回溯
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
该算法的实现主要是贪心+二分查找,最后回溯。以[10,9,2,5,3,7,101,6]举例。
arr = [10, 9, 2, 5, 3, 7, 101, 6]
result = [0] // 初始包含第一个元素的索引
p = arr.slice() // p数组用于回溯
// 完整处理过程:
// 1. i = 1, arrI = 9 // 9 < 10,通过二分查找替换 result = [1] // [9] p[1] = 0
// 2. i = 2, arrI = 2 // 2 < 9,通过二分查找替换 result = [2] // [2] p[2] = 0
// 3. i = 3, arrI = 5 // 5 > 2,直接追加 result = [2, 3] // [2, 5] p[3] = 2
// 4. i = 4, arrI = 3 // 3 > 2 且 3 < 5,替换索引1的值 result = [2, 4] // [2, 3] p[4] = 2
// 5. i = 5, arrI = 7 // 7 > 3,直接追加 result = [2, 4, 5] // [2, 3, 7] p[5] = 4
// 6. i = 6, arrI = 101 // 101 > 7,直接追加 result = [2, 4, 5, 6] // [2, 3, 7, 101] p[6] = 5
// 7. i = 7, arrI = 6 // 6 < 101,通过二分查找找到第一个大于6的位置 // 找到7(索引5),进行替换 result = [2, 4, 5, 7] // [2, 3, 7, 6] p[7] = 4
// 最后进行回溯 u = result.length // u = 4 v = result[u-1] // v = 6 (最后一个索引)
while (u-- > 0) {
result[u] = v
v = p[v]
}
// 最终结果 result = [2, 4, 5, 6] 最终输出:[2, 4, 5, 6],对应的值序列是 [2, 3, 7, 101]关键点解释:
-
虽然在处理最后一个元素6时,确实替换了一个位置,但在最终回溯时,算法选择了能形成最长递增子序列的路径
-
回溯过程通过p数组找到真正的前驱节点,重建了完整的最长递增子序列
-
最终结果 [2, 4, 5, 6] 代表的索引序列,对应的实际值 [2, 3, 7, 101] 确实是原数组中的最长递增子序列
这个算法在Vue的diff过程中用于优化节点移动,通过找到最长递增子序列,可以最小化需要移动的节点数量
//乱序的diff对比
else {
const oldStartIndex = i
const newStartIndex = i
const keyToNewIndexMap = new Map()
for (i = newStartIndex; i <= newChildrenEnd; i++) {
const nextChild = normalizeVNode(newChildren[i])
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
let j
let patched = 0
const toBePatched = newChildrenEnd - newStartIndex + 1
let moved = false
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) {
newIndexToOldIndexMap[i] = 0
}
for (i = oldStartIndex; i <= oldChildrenEnd; i++) {
const prevChild = oldChildren[i]
if (patched >= toBePatched) {
unmount(prevChild)
continue
}
let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
}
if (newIndex === undefined) {
unmount(prevChild)
} else {
newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(prevChild, newChildren[newIndex], container, null)
patched++
}
}
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: []
j = increasingNewIndexSequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = i + newStartIndex
const nextChild = newChildren[nextIndex]
const anchor =
nextIndex + 1 < newChildrenLength
? newChildren[nextIndex + 1].el
: parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild, container, anchor)
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor)
} else {
j--
}
}
}
}
首先建立起新节点到旧节点的map映射,然后遍历旧节点,将不在于在新节点中的旧节点删除,并记录存在于新节点中的旧节点的位置的数组(用于找最长递增子序列),并patch更新。找到最长增长子序列,然后从后向前遍历新节点,(如果是最后一个节点,那么父节点是parent,如果不是最后一个节点,那么anchor是后面一个节点),新增的节点通过patch方法新增,在最长递增子序列的节点保持不动,不在最长递增子序列的节点进行移动。