之前关于快速Diff算法的例子比较理想化,也就是处理完相同的前置节点或后置节点后,新旧两组子节点总会有一组子节点被全度处理完毕。但是有时情况会比较复杂,如下面的例子:
旧子节点:p1,p2,p3,p4,p6,p5
新子节点:p1,p3,p4,p2,p7,p5
在这个例子中,相同的前置节点只有p1,而相同的后置节点只有p5,所以无法简单地通过预处理过程完成更新。
在经过预处理后,无论是新子节点,还是旧子节点,都有部分节点未经处理,这时候旧需要进行进一步的处理。其实无论是简单Diff算法,还是双端Diff算法,还是快速Diff算法,都遵循同样的处理规则
- 判断是否有节点需要移动,以及应该如何移动
- 找出那些需要被添加或移除的节点
接下来就是判断哪些节点需要移动,以及应该如何移动。
首先在非理想的情况下,当相同的前置节点和后置节点都处理完毕后,索引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
// 省略代码
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
// 增加else分支来处理非理想情况
}
}
接下来首先需要构造一个数组source,其长度等于新的一组子节点在经过预处理之后剩余未处理节点的数量,并且source中每个元素的初始值都是-1。
代码如下
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
// oldStart 和 newStart 分别为其实索引,既j
const oldStart = i
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(prevVNode,nextVNode,container)
// 最后填充source数组
source[k-newStart] = i
}
}
}
}
这里要注意的是,由于数组source的索引是从0开始的,而未处理节点的索引则未必是从0开始的,所以在填充数组时要使用表达式k-newStart
的值作为数组的索引值。外层循环的变量i
就是当前节点在旧的一组子节点中的位置索引。
但是这里有个问题,在上面那段代码中使用了嵌套循环,如果新旧子节点的数量较多会带来性能上的问题。出于优化的目的,可以为新的一组子节点构建一张索引表,用来存储节点的key和节点位置索引之间的映射,有了索引表就可以快速德填充source数组,
TIPS: 可以用索引表来解决嵌套循环的问题
代码如下:
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
// oldStart 和 newStart 分别为其实索引,既j
const oldStart = i
const newStart = j
// 构建索引表
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]
// 如果k存在,说明该节点是可服用的
if(typeof k !== 'undefinded'){
newVNode = newChildren[k]
// 调用patch函数完成更新
patch(oldVNode,newVNode,container)
// 填充source数组
source[k - newStart] = i
}else{
// 没找到就卸载
unmount(oldVNode)
}
}
}
接下来要判断节点是否需要移动。实际上和简单Diff算法类似,如下面代码所示:
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
// oldStart 和 newStart 分别为其实索引,既j
const oldStart = i
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(prevVNode,nextVNode,container)
// 最后填充source数组
source[k-newStart] = i
}
}
}
}
这里要注意的事,由于数组source的索引是从0开始的,而未处理节点的索引则未必是从0开始的,所以在填充数组时要使用表达式k-newStart
的值作为数组的索引值。外层循环的变量i就是当前节点在旧的一组子节点中的位置索引。
但是这里有个问题,在上面那段代码中使用了嵌套循环,如果新旧子节点的数量较多会带来性能上的问题。出于优化的目的,可以为新的一组子节点构建一张索引表,用来存储节点的key和节点位置索引之间的映射,有了索引表就可以快速德填充source数组,
TIPS: 索引表来解决嵌套循环的问题
代码如下:
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
const oldStart = i
const newStart = j
// 新增两个变量,mover和pos
let 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]
const k = keyIndex[oldVNode.key]
if(typeof k !== 'undefinded'){
newVNode = newChildren[k]
patch(oldVNode,newVNode,container)
source[k - newStart] = i
// 判断节点是否需要移动
if(k<pos){
moved = true
}else{
pos = k
}
}else{
// 没找到就卸载
unmount(oldVNode)
}
}
}
如果在遍历过程中遇到的索引值呈现递增趋势,则说明不需要移动节点,反之则需要。
除此之外还需要一个数量标识,代表已经更新过的节点数量。已经更新过的节点数量应该小于新的一组子节点中需要更新的节点数量。一旦前者超过后者,则说明有多余的节点,应该将其卸载。代码如下:
if(j>oldEnd && j<=newEnd){
// 省略部分代码
}else if(j>newEnd && j<=oldEnd){
// 省略部分代码
}else{
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
const oldStart = i
const newStart = j
let moved = false
let pos = 0
const keyIndex = {}
for(let i=newStart;i<=newEnd;i++){
keyIndex[newChildren[i].key] = i
}
// 新增patched变量,代表更新过的节点数量
let patched = 0
for(let i=oldStart;i<=oldEnd;i++){
const oldVNode = oldChildren[i]
// 如果更新过的节点数量小于等于需要更新的节点数量,则执行更新
if(patched <= count){
const k = keyIndex[oldVNode.key]
if(typeof k !== 'undefinded'){
newVNode = newChildren[k]
patch(oldVNode,newVNode,container)
// 每更新一个节点,都讲patched变量 +1
patched++
source[k - newStart] = i
if(k<pos){
moved = true
}else{
pos = k
}
}else{
// 没找到
unmount(oldVNode)
}
}else{
// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余的节点
unmount(oldVNode)
}
}
}