Vue底层原理之虚拟DOM(3)详解diff算法--子节点更新策略

本文详细解析了Diff算法在更新虚拟DOM时的子节点更新策略,包括新前与旧前、新后与旧后等四种查找方式。在处理节点新增、删除和移动时,介绍了具体的指针移动和节点处理规则。通过实例分析,展示了Diff算法如何高效地对比新旧节点并生成最小变更的更新方案。
摘要由CSDN通过智能技术生成

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);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值