vue diff算法

310 阅读5分钟

/**

  • diff全称difference,译为中文就是差异、不同的意思,而diff算法顾名思义就是用来得到差一点的,同时我们需要一个本体,一个参照体,通过对本体和参照体的比较得出他们有哪些差异点;

  • 这个过程我们简称找不同,而这也是diff算法的本质

  • 在前端领域,diff算法多用在框架中,如vue、react中;他的本体和参照物都是虚拟dom

  • 什么是虚拟dom:虚拟dom本质上就是一个对象,该对象描述了一个UI节点所对应的一些必要信息

  • 真实dom的crud损耗是固定的,使用虚拟dom的意义在于在diff比对的时候,真实dom上的属性很多,开销很大;所有需要使用虚拟dom

  • 为什么需要虚拟dom?

  • 因为真实dom在创建的时候会携带非常多的属性,我们知道最终在进行diff的时候是需要本体和参照体的,在目前不考虑前端diff的具体实现细节,我们仅从宏观逻辑角度去考虑,至少可以

  • 明确一点:本体和参照物的细节越多,对比所消耗的精力和时间也就越多;而整个真实dom的属性这么多,如果我们都一个一个拿出来比较,无疑是一笔巨大的损耗

  • 前端的diff其实就是毕竟虚拟dom的差异,或者说虚拟dom树的差异

  • 虚拟dom树

  • vue

  • diff算法书写在一个名为patch的函数中

  • 1、如果两个都是文本节点,就看文本值相不相同;如果不同则直接应用型的文本值(这是最不消耗性能的)

  • 2、如果oldNode存在,newNode没有了;那就代表卸载,直接移除对应的dom(这也是没有办法的)

  • 3、如果oldNode不存在,newNode存在;那就代表新增,需要直接新增dom(这也是没有办法的)

  • 4、如果两个节点都不为空,而且还是可以复用的同一个节点;那就直接去比较子元素,在这种情况下绝对不可能是文本节点;新旧两个节点的tag、key都一直的话,节点就可以直接复用;但是需要注意还是要比较其他属性,如id、class

  • 5、如果两个节点都完全不同,那只能以newNode为准了,旧的直接删除 */

    // 构建虚拟dom class Vnode { key; //唯一的key值 isStatic; // vue3中标记的时候是静态节点 componentInstance; // 如果是组件的话,该属性代表组件实例 constructor( tag, // 虚拟dom类型 children, //该虚拟dom的子节点,每一个子节点又是一个新的虚拟dom text, // 该虚拟dom的文本,只有文本节点才有值 elm,// 该虚拟dom对应的真实dom parent, // 该虚拟dom的父节点 data, // 该虚拟节点的属性信息,比如class,props,styles等等

     ){
         // 初始化
         this.tag = tag;
         this.children = children;
         this.text = text;
         this.elm = elm;
         this.parent = parent;
         this.data = data;
     }
    

    }

    const div = new Vnode( "div", ["helloWorld"], null, "div", null, { attrs:{ class:"wrapper" } }

    ) console.log(div,"dvvv") // 判断是否是undefined或者null function isUndef(v){ return v === undefined || v === null; }

    function isDef(v){ return v !==undefined && v!==null; }

    // 判断两个节点是否相同 function isSameNode(a,b){ return ( a.key === b.key && a.tag === b.tag && isDef(a.data) === isDef(b.data) && isSameInputType(a,b)

     )
    

    } // 判断两个输入框的类型是否相同 function isSameInputType(a,b){ if(a.tag!=='input')return true; const typeA = a.data.attrs.type; const typeB = b.data.attrs.type; return typeA === typeB; } // vue diff 算法 function patch(oldNode,newNode){ // 1. 首先,如果oldNode有值,新的没有了,那就直接移除 if(isDef(oldNode) && isUndef(newNode)){ oldNode.elm && oldNode.remove(); } // 2. 如果旧节点没有定义,新节点有值,那就是直接创建 if(isDef(newNode) && isUndef(oldNode)){ newNode.parent.elm.appendChild(newNode.elm) } // 3. 如果两个是一样的节点,就去进行子节点比对 // 判断是不是真实dom const isRealElement = isDef(oldNode.nodeType); if(!isRealElement && isSameNode(oldNode,newNode)){ // isRealElement用户判断整个oldNode是不是虚拟dom // 如果两个是一样的节点,去对比子节点 patchNode(oldNode,newNode) } }

    function patchNode(oldNode,newNode){ // 能够走进这个方法,至少证明了 // 1 oldNode的tag和newNode的tag是一样的 // 2 同时两个节点的key也是一样的,elm也是一样的;所以我们不需要创建新的真实dom // 还需要注意的地方就是,vue3中推出了静态节点,如果该节点被标记为静态节点,就无条件复用 //基于上面的分析,我们在这个方法里首先要看的就是两个节点是不是静态节点 // 静态节点会在初始化的时候给每个节点打上isStatic标记

     if(oldNode === newNode)return;// 如果两个引用完全一样,直接返回
     const elm = newNode.elm = oldNode.elm; // elm直接复用
     if(oldNode.isStatic && newNode.isStatic){
         newNode.componentInstance = oldNode.componentInstance;//如果都是静态节点,直接复用旧节点的组件实例
         return;
     }
    
     // 判断新旧节点是否是文本节点,如果是文本节点,比较文本是否相同。如果不同就应用最新的文本
     if(isDef(newNode.text)){
         if(newNode.text!==oldNode.text){
             // 直接修改真实文本节点的值,页面也会直接变化; vue是一边diff,一边更新的
             newNode.elm.textContent = newNode.text;
         }
         return
     }
     // 如果不是text节点,对比两个节点data属性
     const {attrs:oldAttrs} = oldNode.data
     const {attrs:newAttrs} = newNode.data;
     const attrKeys = Object.values(oldNode);
     attrKeys.forEach(attr=>{
         if(newNode.data[attr]!==oldNode.data[attr]){
             elm.setAttribute(attr,newNode.data[attr])
         }
     })
     // 比较子节点
     const oldChild = oldNode.children;
     const newChild = newNode.children;
     updateChildren(elm,oldChild,newChild);
    

    } // 承载了diff算法的核心 /** *

    • @param {*} parentElm 两个子节点的父节点

    • @param {*} oldCh 旧节点

    • @param {*} newCh 新节点

    • @param {*} insertedVnodeQueue

    • @param {*} removeOnly

    • 比较新子树和旧子树的差异,并且更新真实dom */ function updateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){ let oldStartIndex = 0; // 旧树首节点索引 let newStartIndex = 0; // 新树首节点索引 let oldEndIndex = oldCh.length -1; // 旧树尾节点索引 let newEndIndex = newCh.length -1; // 新树尾节点索引 let oldStartNode = oldCh[oldStartIndex]; // 旧树首节点 let newStartNode = newCh[newStartIndex] // 新树首节点 let oldEndNode = oldCh[oldEndIndex]; // 旧树尾节点 let newEndNode = newCh[newEndIndex]; // 新树尾节点 let oldKeyToIdx,idxInOld,vnodeToMove; // 如果oldStartIndex大于了oldEndIndex,说明循环已经走完了 while(oldStartIndex<=oldEndIndex && newStartIndex<=newEndIndex){ // 判断旧树的首节点是不是空(undefined或者null) if(isUndef(oldStartNode)){ // 如果旧树的首节点没有,那就往下找,找到有为止 oldStartNode = oldCh[++oldStartIndex] }else if(isUndef(oldEndNode)){ // 能够走到这个判断,说明旧树的首节点已经找到,尾节点还没找到 oldEndNode = oldCh[--oldEndIndex] }else if(isSameNode(oldStartNode,newStartNode)){ // 旧树的首节点和新树的尾节点可以复用 // 能走到这个判断,说明 // 1.旧树的首尾节点全部找到了 // 2.旧树的首节点和新树的尾节点可以复用 // updateChild只能比较子元素,虽然dom可以重用,但是不能保证class没有变化 // insertedVnodeQueue 表示已经处理过的虚拟dom,一旦有新增dom的操作的时候就会起作用 patchNode(oldStartNode,newStartNode,insertedVnodeQueue); oldStartNode = oldCh[++oldStartIndex]; newStartNode = newCh[++newStartIndex]; }else if(isSameNode(oldEndNode,newEndNode)){

           patchNode(oldEndNode,newEndNode,insertedVnodeQueue);
           oldEndNode = oldCh[--oldEndIndex]
           newEndNode = newCh[--newEndNode]
       }else if(isSameNode(oldStartNode,newEndNode)){
           // 能够进入到这判断,说明
           // 1. 旧树的首尾节点都有
           // 2. 旧树的首节点和新书的首节点不同
           // 3. 旧树的尾节点和新树的尾节点也不同
           // 看旧树的首节点和新树的尾节点是否相同
           patchNode(oldStartNode,newEndNode,insertedVnodeQueue);
           insesrtBefore(parentElm,oldEndNode.elm,nodeOps.nextSibling(oldEndNode.elm));
           oldStartNode = oldCh[++oldStartIndex]
           newEndNode = newCh[--newEndIndex]
       }else if(isSameNode(oldEndNode,newStartNode)){
           patchNode(oldEndNode,newStartNode,insertedVnodeQueue);
           insesrtBefore(parentElm,oldEndNode.elm,oldStartNode.elm)
           oldEndNode = oldCh[--oldEndIndex]
           newStartNode = newCh[++newStartIndex]
       }else {
           // 能够进入这个判断,说明
           // 1. 旧树的首尾节点都有值
           // 2. 旧树的首节点和新树的首节点无法重用
           // 3. 旧树的首节点和新树的尾节点无法重用
           // 4. 旧树的尾节点和新树的首节点无法重用
           // 5. 旧树的尾节点和新树的尾节点无法重用
           // 把目前没有用到的旧树里的所有节点的key都收集起来了
           // 把新节点的key和存起来的key逐一比较
           if(isUndef(oldKeyToIdx)) oldKeyToIdx= createKeyToOldIdx(oldh,oldStartIndex,oldEndIndex)
           // idxInOld: 代表的是新树的首节点在旧树中能否找到对应的key值,如果能够找到,那么这个idxInOld就是该key值对应的虚拟dom所在的索引位
           idxInOld = isDef(newStartNode.key)?oldKeyToIdx[newStartNode.key]:findIdxInOld(newStartNode,oldCh,oldStartIndex,oldEndIndex)
      
           if(isUndef(idxInOld)){
               // 进入这里,表示把旧树都循环找完了,还是没找到;表示没有任何可以重用的元素,只有直接创建
               createElm(newStartNode, insertedVnodeQueue, parentElm, oldStartNode.elm, false, newCh, newStartIndex)
      
           }else {
               // 找到了可以复用的节点
               vnodeToMove = oldCh[idxInOld] // 拿到了可以和当前新树的首节点复用的旧树中的节点
               if(isSameNode(vnodeToMove,newStartNode)){
                   patchNode(vnodeToMove,newStartNode,insertedVnodeQueue);
                   oldch[idxInOld] = undefined;
                   insesrtBefore(parentElm,vnodeToMove.elm,newStartNode.elm)
               }else {
                   // 还是不能复用,创建新节点
                   createElm(newStartNode, insertedVnodeQueue, parentElm, oldStartNode.elm, false, newCh, newStartIndex)
               }
               newStartNode = newCh[++newStartIndex]
           }
       }
      

      }

      // 如果旧树的首节点index大于了旧树的尾节点index,说明新节点有新增的节点 if(oldStartIndex>oldEndIndex){ refElm = isUndef(newCh[newEndIndex +1])?null:newCh[newEndIndex+1].elm addVnodes(parentElm,refElm,newCh,newStartIndex,newEndIndex,insertedVnodeQueue) }else if(newStartIndex>newEndIndex){ // 新树的newStartIndex大于了newEndIndex,表示新树少了节点 removeVnodes(oldCh,oldStartIndex,oldEndIndex) } }

    function createKeyToOldIdx(children,beginIdx,endIdx){ let key,map = []; for(let i=beginIdx;i<endIdx;i++){ key = children[i].key; if(key){ map[key] = i } } return map; }

    function findIdxInOld(node,oldCh,start,end){ for(let i = start;i<end;i++){ const c = oldCh[i]; if(isDef(c) && isSameNode(node,c)) return i; } }