vue源码解析实例(二)---vue 虚拟DOM篇

  1.  
    1. 我们知道,Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的,这是因为浏览器的标准就把 DOM 设计的非常复杂,所以一个真正的 DOM 元素是非常庞大的,如下所示:
      let div = document.createElement('div')let str = ''for (const key in div) {
      str += key + ''}
      console.log(str)
      
     
    我们可以用JS的计算性能来换取操作DOM所消耗的性能。
     
    DOM-Diff
    path
    整个patch无非就是干三件事:
    • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
    • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
    • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。
    优化更新子节点
    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0 // oldChildren开始索引
        let newStartIdx = 0 /// newChildren开始索引
        let oldEndIdx = oldCh.length - 1 // oldChildren结束索引
        let oldStartVnode = oldCh[0] // oldChildren中所有未处理节点中的最后一个
        let oldEndVnode = oldCh[oldEndIdx] // oldChildren中所有未处理节点中的最后一个
        let newEndIdx = newCh.length - 1 // newChildren结束索引
        let newStartVnode = newCh[0] // newChildren中所有未处理节点中的第一个
        let newEndVnode = newCh[newEndIdx] // newChildren中所有未处理节点中的最后一个
        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
    // 如果oldStartVnode不存在,则直接跳过,比对下一个
          } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 如果新前与旧前节点相同,就把两个节点进行patch更新
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 如果新后与旧后节点相同,就把两个节点进行patch更新
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    // 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
    // 如果不属于以上四种情况,就进行常规的循环比对patch
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    // 如果在oldChildren里找不到当前循环的newChildren里的子节点
            if (isUndef(idxInOld)) { // New element
    // 新增节点并插入到合适位置
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
    // 如果在oldChildren里找到了当前循环的newChildren里的子节点
              vnodeToMove = oldCh[idxInOld]
    // 如果两个节点相同
              if (sameVnode(vnodeToMove, newStartVnode)) {
    // 调用patchVnode更新节点
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
    // canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
                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) {
    /**
    * 如果oldChildren比newChildren先循环完毕,
    * 那么newChildren里面剩余的节点都是需要新增的节点,
    * 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
    */
     
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
    /**
    * 如果newChildren比oldChildren先循环完毕,
    * 那么oldChildren里面剩余的节点都是需要删除的节点,
    * 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
    */
     
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }

     

     
    当开始位置大于结束位置时,表示所有节点都已经遍历过了。
    OK,有了这个概念后,我们开始读源码:
     
    1. 如果oldStartVnode不存在,则直接跳过,将oldStartIdx加1,比对下一个
      // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      	if (isUndef(oldStartVnode)) {
              oldStartVnode = oldCh[++oldStartIdx]
            }
      }
    2. 如果oldEndVnode不存在,则直接跳过,将oldEndIdx减1,比对前一个
      else if (isUndef(oldEndVnode)) {
          oldEndVnode = oldCh[--oldEndIdx]
      }
    3. 如果新前与旧前节点相同,就把两个节点进行patch更新,同时oldStartIdx和newStartIdx都加1,后移一个位置
      else if (sameVnode(oldStartVnode, newStartVnode)) {
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
      }

       

    4. 如果新后与旧后节点相同,就把两个节点进行patch更新,同时oldEndIdx和newEndIdx都减1,前移一个位置
      else if (sameVnode(oldStartVnode, newStartVnode)) {
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
      }

       

    5. 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后,最后把oldStartIdx加1,后移一个位置,newEndIdx减1,前移一个位置
      else if (sameVnode(oldStartVnode, newStartVnode)) {
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
      }

       

    6. 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前,最后把newStartIdx加1,后移一个位置,oldEndIdx减1,前移一个位置
      1. 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]
        }

         

    7. 如果不属于以上四种情况,就进行常规的循环比对patch
    8. 如果在循环中,oldStartIdx大于oldEndIdx了,那就表示oldChildren比newChildren先循环完毕,那么newChildren里面剩余的节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
      if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      }

       

    9. 如果在循环中,newStartIdx大于newEndIdx了,那就表示newChildren比oldChildren先循环完毕,那么oldChildren里面剩余的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除
      else if (newStartIdx > newEndIdx) {
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }

       

    OK,处理完毕,可见源码中的处理逻辑跟我们之前分析的逻辑是一样的。

    总结

    Vue为了避免双重循环数据量大时间复杂度升高带来的性能问题,而选择了从子节点数组中的4个特殊位置互相比对,分别是:新前与旧前,新后与旧后,新后与旧前,新前与旧后。对于每一种情况我们都通过图文的形式对其逻辑进行了分析。最后我们回到源码,通过阅读源码来验证我们分析的是否正确。幸运的是我们之前每一步的分析都在源码中找到了相应的实现,得以验证我们的分析没有错。以上就是Vue中的patch过程,即DOM-Diff算法所有内容了,到这里相信你再读这部分源码的时候就有比较清晰的思路了。
     
posted @ 2021-03-05 17:38  嘉煠  阅读(103)  评论(0编辑  收藏  举报