天天看點

vue 的diff算法是什麼比較方式原理分析小結

是什麼

diff 算法是一種通過同層的樹節點進行比較的高效算法

特點:

  • 比較隻會在同層級進行, 不會跨層級比較
  • 在diff比較的過程中,循環從兩邊向中間比較

diff 算法在很多場景下都有應用,在 vue 中,作用于虛拟 dom 渲染成真實 dom 的新舊 VNode 節點比較

比較方式

深度優先,同層比較

vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結
vue 的diff算法是什麼比較方式原理分析小結

原理分析

當資料發生改變時,set方法會調用Dep.notify通知所有訂閱者Watcher,訂閱者就會調用patch給真實的DOM打更新檔 更新相應的視圖

源碼位置:src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) { // 沒有新節點,直接執行destory鈎子函數
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) // 沒有舊節點,直接用新節點生成dom元素
    } else {
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 判斷舊節點和新節點自身一樣,一緻執行patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            // 否則直接銷毀及舊節點,根據新節點生成dom元素
            if (isRealElement) {

                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                }
                if (isTrue(hydrating)) {
                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                        invokeInsertHook(vnode, insertedVnodeQueue, true)
                        return oldVnode
                    }
                }
                oldVnode = emptyNodeAt(oldVnode)
            }
            return vnode.elm
        }
    }
}
           

patch函數前兩個參數位為oldVnode 和 Vnode ,分别代表新的節點和之前的舊節點,主要做了四個判斷:

  • 沒有新節點,直接觸發舊節點的destory鈎子
  • 沒有舊節點,說明是頁面剛開始初始化的時候,此時,根本不需要比較了,直接全是建立,是以隻調用 createElm
  • 舊節點和新節點自身一樣,通過 sameVnode 判斷節點是否一樣,一樣時,直接調用 patchVnode去處理這兩個節點
  • 舊節點和新節點自身不一樣,當兩個節點不一樣的時候,直接建立新節點,删除舊節點

下面主要講的是patchVnode部分

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果新舊節點一緻,什麼都不做
    if (oldVnode === vnode) {
      return
    }

    // 讓vnode.el引用到現在的真實dom,當el修改時,vnode.el會同步變化
    const elm = vnode.elm = oldVnode.elm

    // 異步占位符
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 如果新舊都是靜态節點,并且具有相同的key
    // 當vnode是克隆節點或是v-once指令控制的節點時,隻需要把oldVnode.elm和oldVnode.child都複制到vnode上
    // 也不用再有其他操作
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果vnode不是文本節點或者注釋節點
    if (isUndef(vnode.text)) {
      // 并且都有子節點
      if (isDef(oldCh) && isDef(ch)) {
        // 并且子節點不完全一緻,則調用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        // 如果隻有新的vnode有子節點
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // elm已經引用了老的dom節點,在老的dom節點上添加子節點
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

        // 如果新vnode沒有子節點,而vnode有子節點,直接删除老的oldCh
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)

        // 如果老節點是文本節點
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }

      // 如果新vnode和老vnode是文本節點或注釋節點
      // 但是vnode.text != oldVnode.text時,隻需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
           

patchVnode主要做了幾個判斷:

  • 新節點是否是文本節點,如果是,則直接更新dom的文本内容為新節點的文本内容
  • 新節點和舊節點如果都有子節點,則處理比較更新子節點
  • 隻有新節點有子節點,舊節點沒有,那麼不用比較了,所有節點都是全新的,是以直接全部建立就好了,建立是指建立出所有新DOM,并且添加進父節點
  • 隻有舊節點有子節點而新節點沒有,說明更新後的頁面,舊節點全部都不見了,那麼要做的,就是把所有的舊節點删除,也就是直接把DOM 删除

子節點不完全一緻,則調用updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 舊頭索引
    let newStartIdx = 0 // 新頭索引
    let oldEndIdx = oldCh.length - 1 // 舊尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一個child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最後一個child
    let newStartVnode = newCh[0] // newVnode的第一個child
    let newEndVnode = newCh[newEndIdx] // newVnode的最後一個child
    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

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,證明diff完了,循環結束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一個child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最後一個child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一個節點
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,繼續循環
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一個節點
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,繼續循環
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一個節點
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,則将oldStartVnode.eml移動到oldEndVnode.elm之後
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一個節點
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,則将oldEndVnode.elm移動到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不比對
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 嘗試在oldChildren中尋找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,說明newStartVnode是一個新的節點
        if (isUndef(idxInOld)) { // New element
          // 建立一個新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比較兩個具有相同的key的新節點是否是同一個節點
          //不設key,newCh和oldCh隻會進行頭尾兩端的互相比較,設key後,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找比對的節點,是以為節點設定key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,則将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移動到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是節點不相同,則建立一個新的節點
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }
           

while循環主要處理了以下五種情景:

  • 當新老 VNode 節點的 start 相同時,直接 patchVnode ,同時新老 VNode 節點的開始索引都加 1
  • 當新老 VNode 節點的 end相同時,同樣直接 patchVnode ,同時新老 VNode 節點的結束索引都減 1
  • 當老 VNode 節點的 start 和新 VNode 節點的 end 相同時,這時候在 patchVnode 後,還需要将目前真實 dom 節點移動到 oldEndVnode 的後面,同時老 VNode 節點開始索引加 1,新 VNode 節點的結束索引減 1
  • 當老 VNode 節點的 end 和新 VNode 節點的 start 相同時,這時候在 patchVnode 後,還需要将目前真實 dom 節點移動到 oldStartVnode 的前面,同時老 VNode 節點結束索引減 1,新 VNode 節點的開始索引加 1
  • 如果都不滿足以上四種情形,那說明沒有相同的節點可以複用,則會分為以下兩種情況:
    • 從舊的 VNode 為 key 值,對應 index 序列為 value 值的哈希表中找到與newStartVnode 一緻 key 的舊的 VNode 節點,再進行patchVnode,同時将這個真實 dom移動到 oldStartVnode 對應的真實 dom 的前面
    • 調用 createElm 建立一個新的 dom 節點放到目前 newStartIdx 的位置

小結

  • 當資料發生改變時,訂閱者watcher就會調用patch給真實的DOM打更新檔
  • 通過isSameVnode進行判斷,相同則調用patchVnode方法
  • patchVnode做了以下操作:
    • 找到對應的真實dom,稱為el
    • 如果都有都有文本節點且不相等,将el文本節點設定為Vnode的文本節點
    • 如果oldVnode有子節點而VNode沒有,則删除el子節點
    • 如果oldVnode沒有子節點而VNode有,則将VNode的子節點真實化後添加到el
    • 如果兩者都有子節點,則執行updateChildren函數比較子節點
  • updateChildren主要做了以下操作:
    • 設定新舊VNode的頭尾指針
    • 新舊頭尾指針進行比較,循環向中間靠攏,根據情況調用patchVnode進行patch重複流程、調用createElem建立一個新節點,從哈希表尋找 key一緻的VNode 節點再分情況操作