diff算法的深入学习--由浅入深循序渐进(一篇文章搞定)

目录

一、感受diff算法的心得:

二、深入diff核心思路:

1. diff处理新旧节点不是同一节点时:

(1)如何定义是不是同一个节点:

(2)编写patch.js函数大体框架:

(3)编写createElement.js函数(此内容属于将虚拟DOM 变为真正DOM):

2. diff处理新旧节点是同一个节点时:

3. 手写新旧节点text的不同情况(patchVnode.js函数):

4. 图示经典diff算法优化策略(四种命题查找):

新节点前面索引与旧节点的前面索引(新前旧前)

新节点的后面索引与旧节点的后面索引(新后旧后)

新节点的后面索引与旧节点的前面索引(新后旧前,节点要进行移动)

新节点的前面索引与旧节点的后面索引(新前旧后,节点要进行移动)

情况分析:

(1)新增情况:

(2)多删除情况:

(3)复杂情况:(此时会进行节点的移动)

 5. 手写子节点更新策略(updateChildren.js):

三、diff算法完整图解:


一、感受diff算法的心得:

1. 最小量更新:

在最小量更新中key很重要,key是这个节点的唯一标识。告诉diff算法在更改前后它们是同一个DOM节点。

2. 虚拟节点:

只有是同一个虚拟节点,才能进行精细比较,否则就是暴力删除旧的,插入新的。

3. 如何定义同一个虚拟节点?:

选择器相同且key相同。

4. 同层比较:

diff算法只进行同层比较,不会进行跨层比较。即使是同一个虚拟节点但是跨层了,那么就不会进行精细算法比较,而是暴力删除旧的,然后插入新的。

二、深入diff核心思路:

1. diff处理新旧节点不是同一节点时:

 


(1)如何定义是不是同一个节点:

老节点的key要和新节点的key相同且新节点的选择器要和老节点的选择器相同。

(2)编写patch.js函数大体框架:

  1. 判断传入的第一个参数是DOM解节点还是虚拟节点?(根据是否有sel属性)如果是DOM节点,此时要包装成虚拟节点。(通过vnode函数)
  2. 判断oldVnode和newVnode是不是同一个节点?(根据key值和sel值),如果不是则要进行暴力删除并插入。
import vnode from './vnode.js'
import createElement from './createElement.js'

/*
* oldVnode是真实DOM,newVnode是虚拟结点
* */

export default function(oldVnode,newVnode){
//    判断传入的第一个参数,是DOM节点还是虚拟节点?(判断其sel的值)
    if(oldVnode.sel ==='' || oldVnode.sel === undefined){
    //    传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
    }
//  判断是不是同一个节点key和选择器都要相同
    if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key){
        console.log("是同一个节点,此为最复杂的情况");
    }else{ //如果不是同一个节点,则以旧结点为标杆添加新节点并删除旧节点。
        console.log("不是同一个节点,暴力插入新的,删除旧的");
        createElement(newVnode,oldVnode.elm);
        
    }
}

(3)编写createElement.js函数(此内容属于将虚拟DOM 变为真正DOM):

功能:真正创建节点,将vnode虚拟节点创建为真是DOM节点,并添加到其虚拟节点vnode的elm属性上。

  • 创建虚拟节点的标签。
  • 判断虚拟节点的子节点是不是文本(text属性不为空并且子元素为undefined并且子元素的长度为0)。

文本直接添加其值。

  • 判断虚拟节点的子节点是不是数组(Array.isArray()和数组子元素的长度)

内部是子节点那就需要递归创建子节点。首先遍历子元素,递归调用createElement创建节点,并将其添加到父节点上。

  • 将创建的真实dom添加到其虚拟dom的elm属性上并返回。
// 真正创建节点,将vnode创建为DOM,是孤儿节点,不进行插入

export default function createElement(vnode){
    console.log("目的是把虚拟节点",vnode,"真正变为DOM但是不插入");
    let domNode = document.createElement(vnode.sel); //创建虚拟节点的选择器
    //有子节点还是有文本?
    if(vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)){
    //    它内部是文字
        domNode.innerText = vnode.text;
    }else if(Array.isArray(vnode.children) && vnode.children.length>0){
    //它内部是子节点,就要递归创建结点
        for(let i = 0;i<vnode.children.length;i++){
    //     得到当前这个children
            let ch = vnode.children[i];
    //     创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM

    //     递归调用创建子节点
            let chDOM = createElement(ch);
    //    将子节点一个一个添加到父节点上
            domNode.appendChild(chDOM);
        }
     }
     //    将真实dom节点添加到虚拟dom的elm属性上并返回
    vnode.elm = domNode;
    //返回elm,elm属性是一个纯DOM对象
    return vnode.elm;
}

再看patch.js函数:

以旧节点为标杆插入新节点并删除老节点。

import vnode from './vnode.js'
//vnode负责创建出虚拟节点
import createElement from './createElement'
import patchVnode from "./patchVnode";

/*
* oldVnode是真实DOM,newVnode是虚拟结点
* */

export default function(oldVnode,newVnode){
//    判断传入的第一个参数,是DOM节点还是虚拟节点?(判断其sel的值)
    if(oldVnode.sel ==='' || oldVnode.sel === undefined){
    //    传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
    }
//  判断是不是同一个节点key和选择器都要相同
    if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key){
        console.log("是同一个节点,此为最复杂情况");
    }else{ //如果不是同一个节点,则以旧结点为标杆添加新节点并删除旧节点。

        //添加新节点,得到真实dom节点
        let newVnodeElm = createElement(newVnode);
        //插入到老节点之前(上树) elm:此元素对应的真正dom节点
        if(oldVnode.elm.parentNode && newVnodeElm){
            oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);
        }
    //    删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
}

2. diff处理新旧节点是同一个节点时:

完善上面的图:在上一个图基础上我们又添加了以下几种情况

  • 判断新旧节点是不是同一对象
  • 判断新节点有没有text的值,因为新节点有text值不管老节点有没有children属性都要改变。
  • 判断老节点有没有children属性,如果没有意味着老节点有text属性,那么只需要清空老节点的text属性并添加新的属性。
  • 如果老节点有children属性,则此时为最复杂的情况需要我们用最优雅的diff算法进行精细化比较


3. 手写新旧节点text的不同情况(patchVnode.js函数):

根据图示完善patch.js中是同一个节点的情况(提出一个单独的函数patchVnode.js):

patchVnode.js:对新老节点是否是同一个节点进行判断

  • 首先判断新老节点是不是同一个对象
  • 接着判断新节点有没有text属性(如果有则直接添加,如果没有看看老节点有没有children属性(如果有则为最复杂的情况,如果没有则清空老节点将新节点的children添加上))
import patch from "./patch"
import createElement from './createElement'
import updateChildren from './updateChildren'

//对比同一个虚拟节点

export default function patchVnode(oldVnode,newVnode){
    //首先判断新旧节点在内存中是否相等
    if(oldVnode === newVnode){
        console.log("在内存中相等");
    }
    //判断newVnode有没有text属性
    if(newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
        //新节点有text属性
        console.log("新Vnode有text属性");
        if(newVnode.text!=oldVnode.text){
            //如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可
            //如果老的elm中是children也会立即消失掉
            oldVnode.elm.innerText = newVnode.text;
        }
    }else{
        console.log("新vnode没有text属性");

        //新节点没有text属性,意味着newVnode有children
        //    现在需要判断oldVnode有没有children

        //判断老的有没有children
        if(oldVnode.children != undefined && oldVnode.children.length>0){

            console.log("老节点有Children此时为最复杂的情况");

        }else{
            //老节点没有children

            //清空老的节点
            oldVnode.elm.innerHTML = '';
            //oldVnode没有children

            for(let i = 0;i<newVnode.children.length;i++){
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

4. 图示经典diff算法优化策略(四种命题查找):

  1. 节点面索引与节点的面索引(新前旧前)

  2. 节点的面索引与节点的面索引(新后旧后)

  3. 节点的面索引与节点的面索引(新后旧前,节点要进行移动)

  4. 节点的面索引与节点的面索引(新前旧后,节点要进行移动)

情况分析:

(1)新增情况:

  1. 首先对比新前与旧前(都相等)不需要执行节点移动操作,更新就行。(此时命中一,命中一则不再判断二)
  2. 指针移动新前与旧前都往后移动,同一节点接着往下移。
  3. 此时旧前移动到旧后指针的下方,循环结束。

     4. 循环条件是 while(新前 <= 新后 && 旧前 <= 旧后),循环结束:只要是旧节点先循环完毕,那么说明新的节点当中是由有剩余节点没有被遍历,那么说明新前指向的节点到新后指向这个节点之间的所有节点是需要新增的节点,直接把这些节点插入到dom中就可以。

(2)多删除情况:

  1. 首先进行新前与旧前指针比较(命中),则两个指针分别向后移动,若命中接着移动。

  2. 移动到新前与旧前不匹配;则进行新后与旧后也不匹配;进行新后与旧前不匹配;进行新前与旧后。

  3. 若都没有匹配则用循环进行查找。发现旧节点中有当前新节点中指针指向的节点,那么将此节点的虚拟dom设置成undefined。

  4. 然后新前指针向后移动,最后到不符合循环条件,退出。

  5. 那么旧前和旧后中间的指针则会被删除掉。

(3)复杂情况:(此时会进行节点的移动)

当(3)新后与旧前 命中:

  1. 当命中时将老节点中命的节点设置为undefined并将其移动到旧后的后面,然后旧前向后移动,新后向前移动
  2. 再进行循环时发现新后与旧前命中,然后将旧前指向的指针设置为undefined将其添加到旧后之后,接着两指针再次移动。
  3. 一直循环进行节点的查找,直到退出。

 

 


 当(4)新前与旧后命中,此时要移动节点。

  1. 当(4)命中移动旧子节点(新前)指向的这个节点到旧前指向节点的前面。(也就是把旧子节点中此节点的虚拟节点设置成undefined,移动真实dom到旧前指向节点的前面)然后指针旧后往前移新前往后移。
  2. 接着进行上面的四种操作若都没有命中则进行循环语句查找,找到后给其一个undefined标记,然后把新前与当前指向的节点插入到旧前节点的前面。

 

 3. 接着新前往后移,再进行4中命中比较。发现都没有则将遍历老节点,老节点里面也没有则将新前插入到旧前节点的前面。

 4. 新节点结束循环后,如果老节点中还有剩余节点,那么旧前和旧后指针中间的节点就是要被删除的节点。

 5. 手写子节点更新策略(updateChildren.js):

patchVnode.js函数补充:在最复杂的情况中调用(updateChildren.js

import patch from "./patch"
import createElement from './createElement'
import updateChildren from './updateChildren'

//对比同一个虚拟节点

export default function patchVnode(oldVnode,newVnode){
    //首先判断新旧节点在内存中是否相等
    if(oldVnode === newVnode){
        console.log("在内存中相等");
    }
    //判断newVnode有没有text属性
    if(newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
        //新节点有text属性
        console.log("新Vnode有text属性");
        if(newVnode.text!=oldVnode.text){
            //如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可
            //如果老的elm中是children也会立即消失掉
            oldVnode.elm.innerText = newVnode.text;
        }
    }else{
        console.log("新vnode没有text属性");
        //判断老的有没有children
        if(oldVnode.children != undefined && oldVnode.children.length>0){

            console.log("老节点有Children此时为最复杂的情况");
            updateChildren(oldVnode.elm,oldVnode.children,newVnode.children);

        }else{
            //新节点没有text属性,意味着newVnode有children
            //    现在需要判断oldVnode有没有children

            //清空老的节点
            oldVnode.elm.innerHTML = '';
            //oldVnode没有children

            for(let i = 0;i<newVnode.children.length;i++){
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

创建updateChildren.js 函数:

  • 首先创建新旧节点的收尾值、新旧节点的收尾指针。
  • 编写判断是否是同意虚拟节点的函数 checkSame。
  • 编写大while循环,循环条件是(oldStartIdx<=oldEndIdx && newStartIdx<=newEndIdx)。
  • 在while循环中编写 四种命中情况和都没有命中的情况。
  • 在while循环中不能先做判断,而是先略过加上undefined标记的节点。
  • 都没有命中的情况下我们需要去老节点中查找新节点指向的节点,我们将老节点的节点及其下标存到keyMap里面。
  • while循环结束后,剩余节点的情况(即为多删除情况和新增情况)。
import patchVnode from "./patchVnode.js"
import patch from "./patch";
import createElement from './createElement'

//判断是否是同一个虚拟节点
function checkSameVnode(a,b){
    return a.sel == b.sel && a.key == b.key;
}

export default function updateChildren(parentElm,oldCh,newCh){
    console.log(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(oldStartIdx<=oldEndIdx && newStartIdx<=newEndIdx){

        //首先不是判断是否命中,而是要略过已经加undefined标记的东西
        if(oldStartVnode == null || oldCh[oldStartIdx] == undefined){
            oldStartVnode = oldCh[++oldStartIdx];
        }else if(oldEndVnode == null || oldCh[oldEndIdx] == undefined){
            oldEndVnode = oldCh[--oldEndIdx];
        }else if(newStartVnode == null || newCh[newStartIdx] == undefined){
            newStartVnode = newCh[++newStartIdx];
        }else if(newEndVnode == null || newCh[newEndIdx] == undefined){
            newEndVnode = newCh[--newEndIdx];
        }else if(checkSameVnode(oldStartVnode,newStartVnode)){
            console.log("1命中");
            patchVnode(oldStartVnode,newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }else if(checkSameVnode(oldEndVnode,newEndVnode)){
            console.log("2命中");
            patchVnode(oldEndVnode,newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }else if(checkSameVnode(oldStartVnode,newEndVnode )){
            console.log("3命中");
            patchVnode(oldStartVnode,newEndVnode);
            //进行插入(新后与旧前命中时,此时要移动节点,移动新前指向的这个节点到老节点的旧后(未处理节点)的后面)
            //如何移动节点,只要插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }else if(checkSameVnode(oldEndVnode,newStartVnode )){
            console.log("4命中");
            patchVnode(oldEndVnode,newStartVnode);
            //进行插入
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm.previousSibling);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }else{
        //    四中都没有命中上
            if(!keyMap){
                keyMap= {};
                //从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
                for(let i = oldStartIdx;i<=oldEndIdx;i++){
                    const key = oldCh[i].key;
                    if(key != undefined){
                        keyMap[key] = i;
                    }
                }
            }
            console.log(keyMap);
        //    寻找当前这项(newStartIdx)在keyMap中映射的位置序号
            const idxInold = keyMap[newStartVnode.key];
            console.log(idxInold);
            if(idxInold == undefined){
            //    判断,如果idxInold是undefined表示它是全新的项
            //   被加入的项(就是newStartVnode这项)现在不是真实dom
                parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm);
            }else{
            //    如果不是undefined,不是全新的项,而是要移动
                const elmToMove = oldCh[idxInold];
                    patchVnode(elmToMove,newStartVnode);
                    //    移动,把这项设置为undefined,,表示已经处理完这项了
                    oldCh[idxInold] = undefined;
                    //    移动 调用insertBdfore也可以实现移动
                    parentElm.insertBefore(elmToMove.elm ,oldStartVnode.elm);
            }
        //    指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }
//    继续看有没有剩余的
//    新增情况
    if(newStartIdx<=newEndIdx){
        console.log("new还有剩余节点没有处理,要加项,要把所有剩余的节点都要插入到oldStartIdx之前");
        //遍历新的newCh,添加到老的没有处理的之前
        for(let i = newStartIdx;i<=newEndIdx;i++){
        //    insertBefore方法可以自动识别null。如果是null就会自动排到队尾去
        //    new[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]),oldCh[oldStartIdx].elm);
        }
    }else if(oldStartIdx<=oldEndIdx){//    删除情况
        console.log("old部分还有节点没有处理完");
    //   批量删除oldStartIdx和oldEndIdx之间的项
        for(let i = oldStartIdx;i<=oldEndIdx;i++){
            if(oldCh[i]){
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }


}

三、diff算法完整图解:


注:资料参考《尚硅谷Vue源码系列课程》。本文源码地址:https://gitee.com/c-fff/diff

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值