Vue3使用该算法实现,快速Diff算法包含预处理步骤,借鉴了纯文本Diff算法的思路
在纯文本Diff算法中,首先进行全等比较,若文本全等,则无需进入核心Diff算法步骤,另外会处理两段文本相同的前缀和后缀,如下所示:
TEXT1: I use vue
TEXT2: I use react
则真正需要进行diff操作的部分是
TEXT1: vue
TEXT2: react
快速Diff算法借鉴了纯文本Diff算法
前置节点处理
对于相同的前置和后置节点,由于位置不变,所以无需移动,只需要在他们之间打补丁,对于前置节点,建立索引j,开启while循环,让索引j递增,直到遇到不相同的节点
function diff2(n1, n2, container) {
const newChildren = n2.children
const oldChildren = n1.children
//更新相同的前置节点
//索引j指向新旧两组子节点的开头
let j = 0
let oldVNode = oldChildren[0]
let newVNode = newChildren[0]
while (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
j++
oldVNode = oldChildren[j]
newVNode = newChildren[j]
}
}
后置节点处理
由于新旧两组子节点的数量可能不同·,所以需要两个索引newEnd和oldEnd,分别指向新旧两组子节点中的最后一个节点,开启while循环,从后向前遍历,递减索引值,直到遇到key值不同的节点
//更新相同的后置节点
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
while (oldVNode.key === newVNode.key) {
patch(newVNode, oldVNode, container)
oldEnd--
newEnd--
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[oldEnd]
}
根据j,newEnd,oldEnd来判断是否有新增节点,删除节点
若 oldEnd < j 成立,并且 newEnd >= j 成立代表有新增节点
若 newEnd < j 成立,并且 oldEnd >= j 成立代表有待删除节点
//更新相同的后置节点
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
while (oldVNode.key === newVNode.key) {
patch(newVNode, oldVNode, container)
oldEnd--
newEnd--
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[oldEnd]
}
//代表有新节点
if (j > oldEnd && j <= newEnd) {
//锚点索引
const anchorIndex = newEnd + 1
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
while (j <= newEnd) {
patch(null, newChildren[j++], container, anchor)
}
} else if (j > newEnd && j <= oldEnd) {
//代表需要卸载旧结点
while (j <= oldEnd) {
unmount(oldChildren[j++])
}
}
判断是否需要DOM移动操作
上述例子较理想化,若存在较复杂的情况时
创建source数组,用来存储新的一组子节点在旧的一组子节点中的位置索引,将由此计算出最长递增子序列,辅助DOM移动
可以使用两层for循环来完成source数组的填充,外层循环用于遍历旧的一组子节点,内层循环用于遍历新的一组子节点,但此时时间复杂度为O(n*),当节点数量较多时,会带来性能问题,因此为新的子节点构建一张索引表,用来存储key和节点位置索引间的映射,能将时间复杂度降至O(n)
//处理非理想情况
const count = newEnd - j + 1
//存储新的一组子节点在旧的一组子节点中的位置索引,据其计算出最长递增子序列
const source = new Array(count)
source.fill(-1)
const oldStart = j
const newStart = j
//moved为true时,代表需要移动
let moved = false
let pos = 0
//建立索引表, 使用一层for循环,降低时间复杂度
const 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
} else {
unmount(oldVNode)
}
}
快速Diff算法判断节点是否需要移动与简单Diff算法类似,如果在遍历过程中遇到的索引值呈递增趋势,则说明不需要移动节点,反之则需要。另外增加一个数量标识,代表已更新过的节点数量,若更新过的节点数量超过新的一组的节点数量,说明有多余节点,应卸载
//处理非理想情况
const count = newEnd - j + 1
//存储新的一组子节点在旧的一组子节点中的位置索引,据其计算出最长递增子序列
const source = new Array(count)
source.fill(-1)
const oldStart = j
const newStart = j
//moved为true时,代表需要移动
let moved = false
let pos = 0
//建立索引表, 使用一层for循环,降低时间复杂度
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i
}
//代表更新过的节点数量
let patched = 0
for (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++
source(k - newStart) = i
//逻辑与简单diff算法类似
if (k < pos) {
moved = true
} else {
pos = k
}
} else {
unmount(oldVNode)
}
} else {
unmount(oldVNode)
}
}
如何移动元素
根据source数组计算最长递增子序列,返回最长递增子序列中元素在source数组中的位置索引,为完成移动,还需创建两个索引值i和s
索引i指向新的一组子节点中的最后一个节点
索引s指向最长递增子序列中的最后一个元素
用for循环让i和s往上移动
if (moved) {
//创建最长递增子序列
const seq = lis(source)
//指向最长递增子序列的最后一个元素
let s = seq.length - 1
//指向新的一组子节点的最后一个元素
let i = count - 1
for (i; i >= 0; i--) {
//说明该节点是全新节点,应将其挂载
if (source[i] === -1) {
//该节点在新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]) {
//证明该节点需要移动
const pos = i + newStart
const newVNode = newChildren[pos]
const nextPos = pos + 1
const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null
insert(newVNode.el, container, anchor)
} else {
//该节点不需要移动,只需要让s指向下一个位置
s--
}
}
}
总结
快速Diff算法在实测中性能最优,借鉴了文本Diff算法的与处理思路,先处理新旧两组子节点中的相同前置节点和相同后置节点,当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或卸载旧节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列,最长递增子序列所指向的节点即为不需要移动的节点