在前面的章节,非理想情况的处理,即在一轮比较过程中,不会命中四个步骤的任何一步。这时,拿新的一组子节点中的头部节点去旧的子节点中寻找可复用的节点,并非总是能找得到。
例如下面的例子:
旧子节点: p1,p2,p3
新子节点:p4,p1,p3,p2
首先进行第一轮比较,发现在四个步骤的比较中都找不到可复用的节点。于是用新子节点的头部节点p4去旧子节点中寻找相同key值的节点,发现在旧的子节点中根本就没有p4节点。这说明p4是个新增节点,那么新增节点p4应该挂载到哪里,其实因为p4是新的一组子节点中的头部节点,所以只需要将其挂载到当前头部节点之前即可。“当前”头部节点指的是,旧的一组子节点中的头部节点所对应的很是DOM节点p1。
下面是完成挂载操作的代码:
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
// 增加两个判断分支,如果头尾部节点为undefined,则说明该节点已经被处理过了,直接跳到下一个位置
if(!oldStartVNode){
oldStartVNode = oldChildren[++oldStartIdx]
}else if(!oldEndVNode){
oldEndVNode = newChildren[--oldEndIdx]
}else if(oldStartVNode.key === newStartVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldStartVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newStartVNode.key){
// 省略部分代码
}else{
const idxInOld = oldChildren.findIndex(
node => node.key === newStartVNode.key
)
if(idxInOld > 0){
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove, newStartVNode, container)
insert(vnodeToMove.el, container, oldStartVNode.el)
oldChildren[idxInOld] = undefined
}else{
// 将newStartVNode作为新节点挂载到头部,使用当前头部节点oldStartVNode.el作为锚点
patch(null,newStartVNode, container, oldStartVNode.el)
}
newStartVNode = newChildren[++newStartIdx]
}
}
当条件idxInOld>0不成立时,说明newStartVNode节点是全新的节点,由于newStartVNode节点是头部节点,所以在调用patch函数挂载节点时,使用oldStartVNode.el作为锚点。
当新节点p4挂载完成后,会进行后续的更新,知道全部更新完成。
但是这样就完美了,并不是,看下面的例子:
旧子节点: p1,p2,p3
新子节点:p4,p1,p2,p3
在上面的例子中,按照双端Diff算法来执行更新,会发现在第二步:比较尾节点的时候,发现p3和p3可以复用,再进行下一轮更新会发现两组的尾节点p2又是可以复用的,再次更新后又会发现两组的尾节点p1又是可以复用的。
在这一轮更新完毕后,由于变量oldStartIdx的值大于oldEndIdx的值,满足更新停止的条件。这时候p4在整个更新过程中被遗漏了,没有得到任何处理,这样说明算法中存在缺陷。为了弥补这个缺陷,需要添加额外的处理代码,如下所示:
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
// 省略部分代码
}
// 循环结束后检查索引值的情况
if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){
// 如果满足条件,则说明有新的节点遗留,需要挂载它们
for(let i=newStartIdx; i<=newEndIdx;i++){
patch(null,newChildren[i],container, oldStartVNode.el)
}
}
这样在while循环结束后增加一个if条件语句,检查四个索引值的情况。如果条件oldEndIdx<oldStartIdx && newStartIdx <= newEndIdx
成立,说明新的一组子节点中有遗留的节点需要作为新节点挂载。而索引值位于newStartIdx和newEndIdx这个区间内的节点都是新节点。于是通过for循环来遍历这个区间的节点并主意挂载。挂载时的锚点仍然使用当前的头部节点oldStartVNode.el,这样就完成了对新增元素的处理。
移除不存在的元素
在解决了新增节点的问题后,再来谈论关于移除元素的问题
看这个例子,
旧子节点: p1,p2,p3
新子节点:p1,p3
可以看到,在新的子节点中p2节点已经不存在。首先按照双端Diff算法的思路执行更新
第一步旧发现头部节点p1,两者的key值相同,可以复用。
执行下一轮更新,会发现在第二步的时候发现尾部节点p3,两者的key值相同,可以复用。在完成此轮更新后,此时变量newStartIdx的值大于变量newEndIdx的值,满足更新停止的条件,于是更新结束。
可是这时旧子节点中存在未被处理的节点,应该将其移除。因此,需要增加额外的代码来处理,如下所示:
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
// 省略部分代码
}
if(oldEndIdx < oldStartIdx && oldStartIdx <= oldEndIdx){
// 添加新节点的代码 此处省略
}else if(newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx){
// 移除操作
for(let i=oldStartIdx; i<= oldEndIdx; i++){
unmount(oldChildren[i])
}
}
与处理新增节点类似,要注意的是索引值位于oldStartIdx和oldEndIdx这个区间内的节点都应该被卸载。