vue的虚拟DOM和diff算法

一 vnode和vdom是什么?

   1.vnode本质就是一个js对象。只不过它是以对象的形式,通过不同的属性去描述一个节点结构。

     2.vdom:就是多个vnode组成的树形结构

二、为什么要引入vdom而不是直接操作真实dom?

 

      • DOM 操作非常耗费性能,频繁操作会触发页面的回流和重绘,严重影响性能
      • 手动管理DOM操作更行逻辑容易出错,特别在负载的UI场景下
      • 虚拟DOM是为了解决上述问题。用JS模拟DOM结构(因为JS执行速度很快)
      • 新旧vnode 对比得出最小的更新范围,最后更新DOM。从而明显的提升了渲染效率

三、vnode是什么样的?

  我们可以用JS模拟一下DOM结构,也就是我们所说的vnode。如下dom结构:

<div id="div1" class="container">
     <p>列表</p>
    <ul style="font-size: 20px>
          <li>a</li>
    </ul>
</div>

    用JS模拟出的结构如下:

复制代码
{
    tag:'div',
    props:{
        id:'div1',
        className:'container'
    },
    children:[
       {
             tag:'p',
             text:'列表'
       },
       {
             tag: 'ul',
             props:{
                     style:'font-size:20px'
              },
              children:[
                     {
                            tag:'li',
                            text:'a'
                     }
               ]
        }
    ]
}
复制代码

  vue在挂载阶段会调用  vm._render() 函数生成虚拟节点vnode,vnode会存储渲染时候需要的信息,如下:

{
    tag, //tag名
    data,//数据
    children,//子dom
    text,//文本内容
    elm,//真实DOM
    key //标识 后续做diff算法需要key标识
  }

  eg:<ul><li>a</li><li>a</li><ul> 生成虚拟dom如下:

四、虚拟DOM--diff 算法

  1. 为什么需要 diff 算法?

      • 上面我们知道通过比较虚拟DOM,得出最小变范围,以最小代价更新DOM。
      • 比较两颗树最小差异的复杂度是o(n^3)第一,遍历tree1;第二,遍历tree2;第三,排序。所以推算出时间复杂度时o(n^3))
      • 这种方式虽然精确,但性能不好。如果1000个节点,那么要就要1亿次。所以这个算法不可用

  2. 优化:

      • 降低复杂度:从o(n^3) 降到接近o(n)
      • 最小化DOM更新:找出变化部分,仅对其进行操作               

      2.虚拟DOM树对比更新的算法优化(diff算法):

      • 只比较同一层级,不跨级比较  
      • tag 不相同或者tag相同但key不同,则认为是不相同的节点,则直接删掉重建,不再深度比较  
      • tag 和 key 两者都相同,则认为是相同节点,不再深度比较     

五、diff算法原理

  diff的目的是找出差异,最小化的更新视图。diff算法发生在视图更新阶段,当数据发生变化时,会对新旧虚拟DOM进行对比,指渲染有变化的部分

  • 1. 新旧DOM树逐层比较,只比较同一层级,不跨级比较
    • 由于树结构是逐层递归,所以整体复杂度接近o(n)
  • 2. 优先比较是否是同一类型的节点
    • 如果不是同类型节点(即tag标签不同),则直接删除旧节点,新节点替换旧节点
    • 如果是同类型节点(即tag标签相同),则继续比较是否有相同的key,即vnode是否相等
  • 3. 然后比较vnode 是否相同。(即tag标签相同,继续比较 key)
    • key 不相同,则表示虚拟节点vnode 不同。直接删除,新节点替换旧节点
    • key 相同,则表示vnode 相同。继续下面的比较 
  • 4.如果是相同节点(vnode相同,tag和key相同),则通过 patchVnode 进行diff
    • 对比新老vnode的属性,有变化的更新到真实dom中,两个节点处理完开始对比子节点
    • newVnode 没有children,oldVnode有children, 也就是新节点是文本,那么直接更新text即可

    •  newVnode有children, oldVnode没有chilren, 那么就创建子节点插入元素中

    •  新老vnode都有chilren,则继续下面的判断

    5. 如果两个父节点相同的话,继续通过updateChildren比较子节点(这一部分也是diff的核心)
    • 新旧节点数组会创建四个指针,分别指向两个节点的首尾。首尾指针不断向中间移动  
    • 旧头新头 比较
      • tag 和 key 相同,则递归比较两个节点的子节点。oldStartIndex++、newStartIndex++
      • tag 和 key 不相同,则继续下面的比较
    • 旧尾新尾 比较 
      • tag 和 key 相同,则递归比较两个节点的子节点。oldEndIndex--、newEndIndex--
      • tag 和 key 不相同,则继续下面的比较
    • 旧头新尾 比较
      • tag 和 key 相同,则递归比较两个节点的子节点。oldStartIndex++、newEndIndex--
      • tag 和 key 不相同,则继续下面的比较
    • 旧尾新头 比较 
      • tag 和 key 相同,则递归比较两个节点的子节点。oldEndIndex--、newStartIndex++
      • tag 和 key 不相同,则继续下面的比较
    • 以上都会不符,采用对比查找。新头指针在旧的列表中查找
      • 未找到,创建dom元素并插入
      • 找到了,比较差异、复用并将其移动到前面去(当前新头指针的位置),旧的移走的位置设为undefined(老的节点为空的话,会自动找下一个节点元素)

五、path算法(或者diff算法)的流程

    vue里面diff算法是通过patch函数来完成的,所以也叫作patch算法

  先看一下vue中patch代码(core/vdom/path.js)

复制代码
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    var isInitialPatch = false;
    var insertedVnodeQueue = [];
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) { //比较元素类型
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); //相同则比较元素
      } else {
          // 省略
          oldVnode = emptyNodeAt(oldVnode);
        }
        // replacing existing element
        var oldElm = oldVnode.elm;
        var parentElm$1 = nodeOps.parentNode(oldElm);
        // create new node
        // 不同则直接创建新的
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm$1,
          nodeOps.nextSibling(oldElm)
        );
        // 省略 parent相关内容
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }
复制代码

patch(oldVnode,vonode)对新老虚拟节点进行对比:

  1. 如果oldVnode是真实元素,则表示是第一次patch(组件第一次挂载的时候,一般是 <div id="app></div>),那么直接 createEllm()创建新的DOM元素

    判断是否是真实元素,是根据节点是否存在nodeType 来判断是否是真实元素

 var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
      }

 

  2.如果新的vnode不存在,老的vnode存在,则说明是组件销毁阶段,需要销毁老节点

  3.如果老的vnode存在并且不是真实元素,新的vnode也存在,则表示是更新阶段,那么就要判断两个虚拟节点是否相同,判断相同节点代码如下

复制代码
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
复制代码

 

      根据上面代码可以得出判断相同的条件主要有两个:

        1. key 必须相同(都是undefined 则也是相同的),这也说明为什么在vue中列表循环时要加 :key="唯一标识"

        2.DOM元素的标签必须相同(比如都是div)

 

    3.1)如果新老vnode不是相同节点,那么就删除旧的创建新的DOM元素

    3.2)如果是相同节点,那么就通过 patchVnode进行diff

      * 对比新老vnode的属性,有变化的更新到真实dom中,两个节点处理完开始对比子节点

      * 新vnode没有children,也就是新节点是文本,那么直接更新text即可

      * 新vnode有chilren,老vnode没有chilren,那么就删除旧的子节点

      * 新老vnode都有chilren,那么调用 updateChildren 比较他们的差异,这一部分就是diff算法的核心。

  下面让我们看看 updateChildren 是如何对孩子们的差异进行比较的

六、updateChildren 孩子差异比较算法,也是diff算法的核心

  先看一下uodateChildren代码:

复制代码
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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, vnodeToMove, refElm
    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
复制代码

 

  对比子节点流程:

    对新老子节点数组使用两个指针,分别指向头和尾,然后不断像中间靠拢进行对比。具体步骤如下:

对比 旧树头指针 和 新树头指针,看看两个节点是否一样。如果不相同,则对比尾指针,如果相同则做如下对比:

  1. 如果一样,则采用深度优先的方式,递归循环这两个节点是否有子节点,如果有就继续比较。
  2. 然后将两个头指针向后移动

 对比 旧树尾指针 和 新树尾指针。看看两节点是否一致,如果不相同,则需要旧头和新尾交叉对比,如果相同则做如下操作 

  1. 递归比较两个节点的子节点
  2. 然后将两个尾指针前移

对比 旧树头指针 和 新树尾指针。如果不相同。则需要旧尾和新头交叉对比,如果相同:

  1. 递归比较两个节点的子节点
  2. 将旧树头指针后移,新树尾指针前移

对比 旧尾指针 和 新树头指针。如果不相同。则需要拿新头指针在旧的列表中查找,如果相同:

  1. 递归比较两个节点的子节点
  2. 将旧尾指针前移,新树头指针后移

新头指针在旧的列表中查找。

  1. 如果未找到:则创建新dom元素
  2. 如果找到了先递归比较两个节点的子节点
  3. 然后将找到的真实dom元素移动到旧树头指针之前
  4. 最后将新树指针后移

  这样一直递归遍历下去,知道整棵树完成对比。借用网上的一张流程:

 

      

 

posted @   yangkangkang  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示