Vue中的diff算法

前言

Vue 數據渲染中最核心的的部分就是 diff算法 的應用,本文從源碼入手,結合實例,一步步解析 diff 算法的整個流程。vue

diff算法簡介

diff算法是一種經過同層的樹節點進行比較的高效算法,避免了對樹進行逐層搜索遍歷,因此時間複雜度只有 O(n)。diff算法的在不少場景下都有應用,例如在 vue 虛擬 dom 渲染成真實 dom 的新舊 VNode 節點比較更新時,就用到了該算法。diff算法有兩個比較顯著的特色:node

一、比較只會在同層級進行, 不會跨層級比較。git

二、在diff比較的過程當中,循環從兩邊向中間收攏github

diff流程

本着對 diff 過程的認識和 vue 源碼的學習,咱們經過 vue 源碼的解讀和實例分析來理清楚 diff 算法的整個流程,下面把整個 diff 流程拆成三步來具體分析:算法

第一步

vue 的虛擬 dom 渲染真實 dom 的過程當中首先會對新老 VNode 的開始和結束位置進行標記:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。c#

let oldStartIdx = 0 // 舊節點開始下標
let newStartIdx = 0 // 新節點開始下標
let oldEndIdx = oldCh.length - 1 // 舊節點結束下標
let oldStartVnode = oldCh[0]  // 舊節點開始vnode
let oldEndVnode = oldCh[oldEndIdx] // 舊節點結束vnode
let newEndIdx = newCh.length - 1 // 新節點結束下標
let newStartVnode = newCh[0] // 新節點開始vnode
let newEndVnode = newCh[newEndIdx] // 新節點結束vnode

通過第一步以後,咱們初始的新舊 VNode 節點以下圖所示:dom

第二步

標記好節點位置以後,就開始進入到的 while 循環處理中,這裏是 diff 算法的核心流程,分狀況進行了新老節點的比較並移動對應的 VNode 節點。while 循環的退出條件是直到老節點或者新節點的開始位置大於結束位置。源碼分析

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    ....//處理邏輯
}

接下來具體介紹 while 循環中的處理邏輯, 循環過程當中首先對新老 VNode 節點的頭尾進行比較,尋找相同節點,若是有相同節點知足 sameVnode(能夠複用的相同節點) 則直接進行 patchVnode (該方法進行節點複用處理),而且根據具體情形,移動新老節點的 VNode 索引,以便進入下一次循環處理,一共有 2 * 2 = 4 種情形。下面根據代碼展開分析:學習

情形一:當新老 VNode 節點的 start 知足sameVnode 時,直接 patchVnode 便可,同時新老 VNode 節點的開始索引都加1。
if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
     }
情形二:當新老 VNode 節點的 end 知足 sameVnode 時,一樣直接 patchVnode 便可,同時新老 VNode 節點的結束索引都減1。
else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      }
情形三:當老 VNode 節點的 start 和新 VNode 節點的 end 知足 sameVnode 時,這說明此次數據更新後 oldStartVnode 已經跑到了 oldEndVnode 後面去了。這時候在 patchVnode 後,還須要將當前真實 dom 節點移動到 oldEndVnode 的後面,同時老 VNode 節點開始索引加1,新 VNode 節點的結束索引減1。
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]
      }
情形四:當老 VNode 節點的 end 和新 VNode 節點的 start 知足 sameVnode 時,這說明此次數據更新後 oldEndVnode 跑到了 oldStartVnode 的前面去了。這時候在 patchVnode 後,還須要將當前真實 dom 節點移動到 oldStartVnode 的前面,同時老 VNode 節點結束索引減1,新 VNode 節點的開始索引加1。
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      }

若是都不知足以上四種情形,那說明沒有相同的節點能夠複用,因而則經過查找事先創建好的以舊的 VNode 爲 key 值,對應 index 序列爲 value 值的哈希表。從這個哈希表中找到與 newStartVnode 一致 key 的舊的 VNode 節點,若是二者知足 sameVnode 的條件,在進行 patchVnode 的同時會將這個真實 dom 移動到 oldStartVnode 對應的真實 dom 的前面;若是沒有找到,則說明當前索引下的新的 VNode 節點在舊的 VNode 隊列中不存在,沒法進行節點的複用,那麼就只能調用 createElm 建立一個新的 dom 節點放到當前 newStartIdx 的位置。spa

else {// 沒有找到相同的能夠複用的節點,則新建節點處理
        /* 生成一個key與舊VNode的key對應的哈希表(只有第一次進來undefined的時候會生成,也爲後面檢測重複的key值作鋪墊) 好比childre是這樣的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 結果生成{key0: 0, key1: 1, key2: 2} */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*若是newStartVnode新的VNode節點存在key而且這個key在oldVnode中能找到則返回這個節點的idxInOld(即第幾個節點,下標)*/
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode沒有key或者是該key沒有在老節點中找到則建立一個新的節點*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          /*獲取同key的老節點*/
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            /*若是新VNode與獲得的有相同key的節點是同一個VNode則進行patchVnode*/
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            //由於已經patchVnode進去了,因此將這個老節點賦值undefined
            oldCh[idxInOld] = undefined
            /*當有標識位canMove實能夠直接插入oldStartVnode對應的真實Dom節點前面*/
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            /*當新的VNode與找到的一樣key的VNode不是sameVNode的時候(好比說tag不同或者是有不同type的input標籤),建立一個新的節點*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }

再來看咱們的實例,第一次循環後,找到了舊節點的末尾和新節點的開頭(都是D)相同,因而直接複用 D 節點做爲 diff 後建立的第一個真實節點。同時舊節點的 endIndex 移動到了 C,新節點的 startIndex 移動到了 C。

緊接着開始第二次循環,第二次循環後,一樣是舊節點的末尾和新節點的開頭(都是C)相同,同理,diff 後建立了 C 的真實節點插入到第一次建立的 B 節點後面。同時舊節點的 endIndex 移動到了 B,新節點的 startIndex 移動到了 E。

接下來第三次循環中,發現 patchVnode 的4種情形都不符合,因而在舊節點隊列中查找當前的新節點 E,結果發現沒有找到,這時候只能直接建立新的真實節點 E,插入到第二次建立的 C 節點以後。同時新節點的 startIndex 移動到了 A。舊節點的 startIndex 和 endIndex 都保持不動。

第四次循環中,發現了新舊節點的開頭(都是A)相同,因而 diff 後建立了 A 的真實節點,插入到前一次建立的 E 節點後面。同時舊節點的 startIndex 移動到了B,新節點的startIndex 移動到了B。

第五次循環中,情形同第四次循環同樣,所以 diff 後建立了 B 真實節點 插入到前一次建立的 A 節點後面。同時舊節點的 startIndex 移動到了C,新節點的 startIndex 移動到了F。

這時候發現新節點的 startIndex 已經大於 endIndex 了。再也不知足循環的條件了。所以結束循環,接下來走後面的邏輯。

第三步

當 while 循環結束後,根據新老節點的數目不一樣,作相應的節點添加或者刪除。若新節點數目大於老節點則須要把多出來的節點建立出來加入到真實 dom 中,反之若老節點數目大於新節點則須要把多出來的老節點從真實 dom 中刪除。至此整個 diff 過程就已經所有完成了。

if (oldStartIdx > oldEndIdx) {
      /*所有比較完成之後,發現oldStartIdx > oldEndIdx的話,說明老節點已經遍歷完了,新節點比老節點多, 因此這時候多出來的新節點須要一個一個建立出來加入到真實Dom中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) //建立 newStartIdx - newEndIdx 之間的全部節點
    } else if (newStartIdx > newEndIdx) {
      /*若是所有比較完成之後發現newStartIdx > newEndIdx,則說明新節點已經遍歷完了,老節點多於新節點,這個時候須要將多餘的老節點從真實Dom中移除*/
      removeVnodes(oldCh, oldStartIdx, oldEndIdx) //移除 oldStartIdx - oldEndIdx 之間的全部節點
    }

再回過頭看咱們的實例,新節點的數目大於舊節點,須要建立 newStartIdx 和 newEndIdx 之間的全部節點。在咱們的實例中就是節點 F,所以直接建立 F 節點對應的真實節點放到 B 節點後面便可。

最後

經過上述的源碼和實例的分析,咱們完成了 Vue 中 diff 算法的完整解讀。若是想要了解更多的 Vue 源碼。歡迎進入咱們的github進行查看,裏面有Vue源碼分析另外幾篇文章,另外對 Vue 工程的每一行源碼都作了註釋,方便你們的理解。~~~~

相關文章
相關標籤/搜索