您好,欢迎来到榕意旅游网。
搜索
您的当前位置:首页Vue中虚拟dom比较原理的介绍(示例讲解)

Vue中虚拟dom比较原理的介绍(示例讲解)

来源:榕意旅游网

所有数据比较完后,就到子节点的比较了。先判断当前vnode是否为文本节点,如果是文本节点就不用考虑子节点的比较;若是元素节点,就需要分三种情况考虑:

  • 新旧节点都有children,那就进入子节点的比较(diff算法);

  • 新节点有children,旧节点没有,那就循环创建dom节点;

  • 新节点没有children,旧节点有,那就循环删除dom节点。

  • 后面两种情况都比较简单,我们直接对第一种情况,子节点的比较进行分析。

    diff算法

    子节点比较这部分代码比较多,先说说原理后面再贴代码。先看一张子节点比较的图:

    1177457578-5c6a2228c0058_articlex.png

    图中的oldChnewCh分别表示新旧子节点数组,它们都有自己的头尾指针oldStartIdxoldEndIdxnewStartIdxnewEndIdx,数组里面存储的是vnode,为了容易理解就用a,b,c,d等代替,它们表示不同类型标签(p,span,p)的vnode对象。

    子节点的比较实质上就是循环进行头尾节点比较。循环结束的标志就是:旧子节点数组或新子节点数组遍历完,(即 oldStartIdx > oldEndIdx || newStartIdx > newEndIdx)。大概看一下循环流程:

  • 第一步 头头比较。若相似,旧头新头指针后移(即 oldStartIdx++ && newStartIdx++),真实dom不变,进入下一次循环;不相似,进入第二步。

  • 第二步 尾尾比较。若相似,旧尾新尾指针前移(即 oldEndIdx-- && newEndIdx--),真实dom不变,进入下一次循环;不相似,进入第三步。

  • 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即 oldStartIdx++ && newEndIdx--),未确认dom序列中的头移到尾,进入下一次循环;不相似,进入第四步。

  • 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即 oldEndIdx-- && newStartIdx++),未确认dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。

  • 第五步 若节点有key且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移(即 newStartIdx++);否则,vnode对应的dom(vnode[newStartIdx].elm)插入当前真实dom序列的头部,新头指针后移(即 newStartIdx++)。

  • 先看看没有key的情况,放个动图看得更清楚些!

    4038085637-5c6c06c2ecbaf_articlex.gif

    相信看完图片有更好的理解到diff算法的精髓,整个过程还是比较简单的。上图中一共进入了6次循环,涉及了每一种情况,逐个叙述一下:

  • 第一次是头头相似(都是a),dom不改变,新旧头指针均后移。a节点确认后,真实dom序列为:a,b,c,d,e,f,未确认dom序列为:b,c,d,e,f

  • 第二次是尾尾相似(都是f),dom不改变,新旧尾指针均前移。f节点确认后,真实dom序列为:a,b,c,d,e,f,未确认dom序列为:b,c,d,e

  • 第三次是头尾相似(都是b),当前剩余真实dom序列中的头移到尾,旧头指针后移,新尾指针前移。b节点确认后,真实dom序列为:a,c,d,e,b,f,未确认dom序列为:c,d,e

  • 第四次是尾头相似(都是e),当前剩余真实dom序列中的尾移到头,旧尾指针前移,新头指针后移。e节点确认后,真实dom序列为:a,e,c,d,b,f,未确认dom序列为:c,d

  • 第五次是均不相似,直接插入到未确认dom序列头部。g节点插入后,真实dom序列为:a,e,g,c,d,b,f,未确认dom序列为:c,d

  • 第六次是均不相似,直接插入到未确认dom序列头部。h节点插入后,真实dom序列为:a,e,g,h,c,d,b,f,未确认dom序列为:c,d

  • 但结束循环后,有两种情况需要考虑:

  • 新的字节点数组(newCh)被遍历完(newStartIdx > newEndIdx)。那就需要把多余的旧dom(oldStartIdx -> oldEndIdx)都删除,上述例子中就是c,d

  • 新的字节点数组(oldCh)被遍历完(oldStartIdx > oldEndIdx)。那就需要把多余的新dom(newStartIdx -> newEndIdx)都添加。

  • 上面说了这么多都是没有key的情况,说添加了:key可以优化v-for的性能,到底是怎么回事呢?因为v-for大部分情况下生成的都是相同tag的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样),那加了key会有什么帮助呢?这边引用一张图:

    3822224183-5c6c0c41aae6f_articlex.png

    key的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!

    有兴趣的可以看看代码:

    function updateChildren (parentElm, oldCh, newCh) {
     let oldStartIdx = 0
     let newStartIdx = 0
     let oldEndIdx = oldCh.length - 1
     let oldStartVnode = oldCh[0]
     let oldEndVnode = oldCh[oldEndIdx]
     let newEndIdx = newCh.length - 1
     let newStartVnode = newCh[0]
     let newEndVnode = newCh[newEndIdx]
     let oldKeyToIdx, idxInOld, elmToMove, before
    
     while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
     if (isUndef(oldStartVnode)) {
     oldStartVnode = oldCh[++oldStartIdx] // 未定义表示被移动过
     } else if (isUndef(oldEndVnode)) {
     oldEndVnode = oldCh[--oldEndIdx]
     } else if (sameVnode(oldStartVnode, newStartVnode)) { // 头头相似
     patchVnode(oldStartVnode, newStartVnode)
     oldStartVnode = oldCh[++oldStartIdx]
     newStartVnode = newCh[++newStartIdx]
     } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相似
     patchVnode(oldEndVnode, newEndVnode)
     oldEndVnode = oldCh[--oldEndIdx]
     newEndVnode = newCh[--newEndIdx]
     } else if (sameVnode(oldStartVnode, newEndVnode)) { // 头尾相似
     patchVnode(oldStartVnode, newEndVnode)
     api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
     oldStartVnode = oldCh[++oldStartIdx]
     newEndVnode = newCh[--newEndIdx]
     } else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾头相似
     patchVnode(oldEndVnode, newStartVnode)
     api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
     oldEndVnode = oldCh[--oldEndIdx]
     newStartVnode = newCh[++newStartIdx]
     } else {
     // 根据旧子节点的key,生成map映射
     if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
     // 在旧子节点数组中,找到和newStartVnode相似节点的下标
     idxInOld = oldKeyToIdx[newStartVnode.key]
     if (isUndef(idxInOld)) { 
     // 没有key,创建并插入dom
     api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm)
     newStartVnode = newCh[++newStartIdx]
     } else {
     // 有key,找到对应dom ,移动该dom并在oldCh中置为undefined
     elmToMove = oldCh[idxInOld]
     patchVnode(elmToMove, newStartVnode)
     oldCh[idxInOld] = undefined
     api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
     newStartVnode = newCh[++newStartIdx]
     }
     }
     }
     // 循环结束时,删除/添加多余dom
     if (oldStartIdx > oldEndIdx) {
     before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm
     addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
     } else if (newStartIdx > newEndIdx) {
     removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
     }
    }

    Copyright © 2019- nryq.cn 版权所有

    违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

    本站由北京市万商天勤律师事务所王兴未律师提供法律服务