diff算法子节点更新策略
diff算法提出了四种命中查找:
- 新前与旧前
- 新后与旧后
- 新后与旧前
- 新前与旧后
命中一种该节点就不再进行判断,都没命中用循环来查找。在四种命中查找中,前指针只会后移,后指针只会前移。四种命中查找循环的条件是:新前<=新后&&旧前<=旧后
新增:
上图中,右边是我们的新节点,它要替代左边的旧节点,li标签是节点的子节点。图里这种情况是新增了两个节点。首先,新前与旧前,A与A相同,两个指针都下移,B与B,C与C都相同,“新前”移动到了D,“旧前”移动到了“旧后”的下边:
旧前>旧后,不满足循环条件,结束循环。
D和E还没有被扫描,循环就结束了,说明DE就是新增的节点。
删除:
与旧节点相比,新节点删除了C,首先还是新前与旧前,A与A,B与B都相同,“新前”移到了D,与“新后”重合,“旧前”移到了C,D与C不相同,于是进入四种命中查找的第二种:新后与旧后,“旧后”指向的是D,D与D相同,两指针前移,“旧后”移到C,“新后”移到B,C与B不相同。此时,新后<新前,不满足循环条件,循环结束。
而如果新节点循环完毕,旧节点中还有剩余节点,说明剩余节点就是要删除的节点,也就是C节点。
再来一个删除的情况:

新节点比旧节点少了两个节点,查找新前与旧前,A跟B都对应上了,于是“新前”移到了D,“旧前”移到了C:

DC不相同,进行第二种查找,新后与旧后,“新后”指向D,“旧后”指向E,也不相同,循环还没有结束,于是第三种查找,新后与旧前,“新后”是D,“旧前”是C,不相同,新前与旧后,DE还是不相同。四种命中都没有查找到,这种情况就需要用循环来查找。
具体操作就是:新节点当前排查到了D,于是在旧节点中循环遍历,找到了D,就把旧D标为undefined(标的是虚拟D节点),新D要插入的位置为所有未处理的节点之前(C的前边),再然后“新前”下移,新前>新后了,循环结束。此时,“旧前”与“旧后”之间的节点就是没有处理过的节点(CE),删除。
到这里,规则还算简单。因为还没有遇到后两种(新后与旧前、新前与旧后)被命中的情况。
我们来个复杂的情况,当后两种命中查找(新后与旧前、新前与旧后)被命中时,指针不是简单的上移或者下移,如果新前与旧后命中,应该把**“旧后”所指向的节点移动到“旧前”所指向节点的前边**,如果新后与旧前命中,应该把**“旧前”所指向的节点移动到“旧后”所指向节点的后边**。也就是,根据新前更新旧后,根据新后更新旧前。

上图中,新节点比旧节点,既有删除(ABD),又有新增(M),而且EC的位置发生了变化。
首先,查找新前与旧前,AE不一致,进行第二种查找,新后与旧后,EM不一致,进行第三种查找,新后与旧前,AM不一致,第四种新前与旧后,EE相同,命中了。
如果新前与旧后命中,应该把**“旧后”所指向的节点移动到“旧前”所指向节点的前边**,而原来的E,在旧节点中记为undefined(只是把E的虚拟节点变成undefined,真实的DOM节点在“旧前”的前边),然后,“新前”下移,“旧后”上移,如图所示:
此时,“新前”与“旧后”又不一致了。指针的位置发生了变化,于是重新进行四种查找,图中四个指针指向的分别是ADCM,显然四种查找都不会命中,都没命中用循环来查找,C在旧节点中被找到,C标为undefined,移动到“旧前”的前边(未被处理过节点之前):
“新前”下移到M,再次遍历四种命中查找,显然,还是没有命中的,又要用循环来查找,循环旧节点发现没有M,M移动到“旧前”的前边:
“新前”下移,新前>新后,循环结束。此时的旧节点还没有都处理完,则“旧前”与“旧后”之间的节点全部删除。
你还清醒吗?

最后来一个极端的例子,在前边描述的移动规则中,还没有遇到命中新后与旧前,如果新后与旧前命中,应该把旧前所指向的节点移动到旧后所指向节点的后边:
图中,新节点完全将旧节点的顺序倒了过来,还是先比较新前与旧前,没有命中,新后与旧后,也没有命中,新后与旧前都是A,命中,于是把旧前指向的节点A移动到旧后所指向节点E的后边,原来的A变为undefined,新后上移,旧前下移。
重新进行四种命中查找,又是命中新后与旧前,B(旧前所指)移到E的后边(旧后之后),旧前的B变为undefined,新后上移,旧前下移。容易发现,之后的操作类似,一直到新后移动到D,旧前也移动到了D,D放到E的后边,旧前的D变为undefined,新后上移到E,旧前下移到E,注意这个时候,新前指向的也是E,所以最后一次遍历四种,新前与旧前匹配上了,新前下移,新前>新后,结束。
手写diff算法
import patchVnode from './patchVnode'
import createElement from './createElement'
// 判断是否是相同节点
function checkSameVnode(a,b){
return (a.sel==b.sel&&a.key==b.key);
}
export default function updateChildren(parentElm, oldCh, newCh) {
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap=null
// 循环
while(oldEndIdx>=oldStartIdx&&newEndIdx>=newStartIdx){
// 首先要略过被打上undefined的节点
if(oldStartVnode==null||oldStartVnode==undefined){
oldStartVnode=oldCh[++oldStartIdx];
}else if(oldEndVnode==null||oldEndVnode==undefined){
oldEndVnode=oldCh[--oldEndIdx];
}else if(newStartVnode==null||newStartVnode==undefined){
newStartVnode=newCh[++newStartIdx];
}else if(newEndVnode==null||newEndVnode==undefined){
newEndVnode=newCh[--newEndIdx];
}
// 新前与旧前
if(checkSameVnode(oldStartVnode,newStartVnode)){// 是相同节点
patchVnode(oldStartVnode,newStartVnode);
oldStartVnode=oldCh[++oldStartIdx];
newStartVnode=newCh[++newStartIdx];
}else if(checkSameVnode(oldEndVnode,newEndVnode)){ //新后与旧后
patchVnode(oldEndVnode,newEndVnode);
oldEndVnode=oldCh[--oldEndIdx];
newEndVnode=newCh[--newEndIdx];
}else if(checkSameVnode(newEndVnode,oldStartVnode)){//新后与旧前
patchVnode(oldStartVnode,newEndVnode);
// 移动节点
parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling)//插入到旧后的后边
newEndVnode=newCh[--newEndIdx];
oldStartVnode=oldCh[++oldStartIdx];
}else if(checkSameVnode(newStartVnode,oldEndVnode)){//新前与旧后
patchVnode(oldEndVnode,newStartVnode);
// 移动节点
parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)
newStartVnode=newCh[++newStartIdx];
oldEndVnode=oldCh[--oldEndIdx];
}else{//都没有命中
if(!keyMap){
keyMap={};
for(let i=oldStartIdx;i<=oldEndIdx;i++){
let key=oldCh[i].key
if(key!=undefined){
keyMap[key]=i;
}
}
}
console.log(keyMap)// Object { A: 0, B: 1, C: 2, D: 3 }
// 寻找当前这项(newStartVnode)在keyMap中的映射序号
const idxInOld=keyMap[newStartVnode.key];
console.log(idxInOld)
if(idxInOld==undefined){// 说明没有这项
parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm);
}else{// 不是新项,说明是要移动
const elmToMove=oldCh[idxInOld];
patchVnode(elmToMove,newStartVnode);
// 把这项设置为undefined,表示处理过了
oldCh[idxInOld]=undefined;
parentElm.insertBefore(elmToMove.elm,oldStartVnode.elm)
}
newStartVnode=newCh[++newStartIdx]
}
}
// 循环结束,看有没有剩余节点
if(oldStartIdx<=oldEndIdx){//旧的有剩余,删除
for(let i=oldStartIdx;i<=oldEndIdx;i++){
// console.log('删除')
if(oldCh[i]){
parentElm.removeChild(oldCh[i].elm);
}
}
}
if(newStartIdx<=newEndIdx){//新的有剩余,新增
// 插入的标杆
const before=newCh[newEndIdx+1]==null?null:newCh[newEndIdx+1].elm;
// 将剩余的循环插入
for(let i=newStartIdx;i<=newEndIdx;i++){
// insertBefore可以识别null,如果是null,排到队尾,前边判断null,是因为null不能打点elm
parentElm.insertBefore(createElement(newCh[i]),before);
}
}
}
本文详细解析了Diff算法在更新虚拟DOM时的子节点更新策略,包括新前与旧前、新后与旧后等四种查找方式。在处理节点新增、删除和移动时,介绍了具体的指针移动和节点处理规则。通过实例分析,展示了Diff算法如何高效地对比新旧节点并生成最小变更的更新方案。
9883

被折叠的 条评论
为什么被折叠?



