在前面,用的都是比较理想的例子。双端Diff算法的每一轮比较的过程都分为四个步骤。理想的情况是,每一轮比较都会命中四个步骤中的一个,但实际上,并不是所有情况都这么理想,例如下面这个例子
旧的一组子节点:[p1,p2,p3,p4]
新的一组子节点:[p2,p4,p1,p3]
如果尝试按照双端Diff算法的思路来进行第一轮比较时,会发现无法命中四个步骤中的任何一步,也就是:
第一步:旧头部节点p1和新头部节点p2
第二步:旧尾部节点p4和新尾部节点p3
第三步:旧头部节点p1和新尾部节点p3
第四步:旧尾部节点p4和新头部节点p2
在四个步骤的比较过程中,都无法找到可复用的节点,那只能通过增加额外的处理步骤来处理这种非理想情况,也就是尝试看看非头部、非尾部的节点能否复用。具体做法就是,拿新的一组子节点中的头部节点去旧的一组子节点中寻找,如下面代码所示:
while(oldStartIndex <= oldEndIdx && newStartIdx <= newEndIdx){
if(oldStartVNode.key === newStartVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldStartVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newStartVNode.key){
// 省略部分代码
}else{
// 遍历旧的一组子节点,试图寻找与newStartVNode拥有相同key值的节点
// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
const idxInOld = oldChildren.findIndex(
node => node.key === newStartVNode.key
)
}
}
这样做其实就是,在旧的一组子节点中找到与新的一组子节点的头部节点具有相同key值的节点。
在上面的例子 新子节点的头部节点p2 会在旧子节点的索引为1的位置找到可复用的节点,也就是说更新后,p2应该变成头部节点。所以要将节点p2对应的真实DOM节点移动到当前旧的一组子节点的头部节点p1所对应的真实DOM节点之前,具体实现如下:
while(oldStartIndex <= oldEndIdx && newStartIdx <= newEndIdx){
if(oldStartVNode.key === newStartVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldStartVNode.key === newEndVNode.key){
// 省略部分代码
}else if(oldEndVNode.key === newStartVNode.key){
// 省略部分代码
}else{
// 遍历旧children,视图寻找与newStartVNode拥有相同key值的元素
const idxInOld = oldChildren.findIndex(
node => node.key === newStartVNode.key
)
// 如果idxInOld大于0,说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
if(idxInOld > 0){
// idInOld位置对应的vnode就是需要移动的节点
const vnodeToMove = oldChildren[idInOld]
// 移动外还应该打补丁
patch(vnodeToMove, newStartVNode, container)
// 将vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
insert(vnodeToMove.el, container, oldStartVNode.el)
// 由于位置idxInOld处的节点所对应的真实DOM已经移动到了别处,因此将其设置为undefined
oldChildren[idxInOld] = undefined
// 最后更新 newStartIdx 到下一个位置
newStartVNode = newChildren[++newStartIdx]
}
}
}
这里要注意的是
- 由于处于idxInOld处的节点已经处理过了(对应的真实DOM移动了别处),因此应该将oldChildren[idxInOld]设置为undefined
- 新的一组子节点中的头部节点已经处理完毕,因此将newStartIdx前进到下一个位置
经过上述两步的操作后,新旧两组子节点以及真实DOM节点的状态如下
旧的一组子节点:[p1,undefined,p3,p4]
新的一组子节点:[‘p2’,p4,p1,p3]
真实的DOM顺序是 p2移动到p1前即 [p2,p1,p3,p4]
双端Diff算法会继续进行:
在第二轮中会发现旧子节点尾部节点p4与新子节点的头部节点p4两者的key值相同,可以复用。此时真实DOM节点的顺序是 [p2,p4,p1,p3]
接着开始下一步的比较,发现在第一步旧子节点的头部节点p1和新子节点的头部节点p1,两者的key值相同,可以复用
由于两者都处于头部,所以不需要对真实DOM进行移动,只需要打补丁即可。
此时真实DOM节点的顺序是[p2,p4,p1,p3],再进行下一轮比较。要注意的是,此时旧子节点的头部节点是undefined,说明该节点已经被处理了,因此不用再处理了,直接跳过即可。为此,需要补充这部分逻辑的代码,具体实现如下:
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
newStartVNode = newChildren[++newStartIdx]
}
}
}
上面的代码,在循环开始时,优先判断头部节点和尾部节点是否存在,如果不存在,则说明已经被处理过了,直接跳到下一个位置即可,在这一轮比较过后,新旧两组子节点与真实DOM节点的状态如图:
接着做最后一轮的比较
比较旧子节点的头部节点p3和新子节点的头部节点p3,两者的key值相同,可以复用
这样在第一步旧找到了可复用的节点。由于都是头部节点,因此不需要进行DOM移动操作,直接打补丁即可,这样就更新完成,最终,真实DOM节点的顺序与新的一组子节点的顺序一致。