vue源碼閱讀(六):diff 算法

前言

vue中,首先是將模板編譯成虛擬DOM,而後再將虛擬DOM轉爲真實的DOM。當咱們的頁面有更新時,僅僅是改變了很小的一部分,要去替換總體舊的DOM的話,勢必會影響性能和用戶體驗的。因此vue中使用diff算法來優化DOM的更新渲染。vue

createPatchFunction

在將虛擬DOM轉爲真實DOM中,有一個很重要的函數,就是createPatchFunction。其中又有一段很重要的代碼。node

return function patch(oldVnode, vnode, hydrating, removeOnly) {
    ...
    // 沒有舊節點,直接生成新節點
    if (isUndef(oldVnode)) {
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 先用 sameVnode 判斷新舊節點是否同樣,同樣的話,
      // 就用 patchVnode 找不同的地方,好比說子節點之類的
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 建立新節點
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // 銷燬舊節點
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    ...
    return vnode.elm
  }
複製代碼

這裏分爲三種狀況,算法

  • 一、沒有舊節點:直接建立新節點
  • 二、有舊節點,可是和新節點不同:建立新節點,刪除舊節點
  • 三、有舊節點,可是和新節點同樣:進入patchVnode

前兩種狀況,以前的文章中,已經講過。接下來,咱們就詳細看看patchVnode數組

patchVnode

function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) {
    // ...
    // 新舊節點徹底一致,直接返回
    if (oldVnode === vnode) {
      return
    }
    // 將舊節點上的 DOM,添加到新節點上。以後新舊節點如有不一致,直接修改 elm 便可
    const elm = vnode.elm = oldVnode.elm

    const oldCh = oldVnode.children
    const ch = vnode.children
    // 新節點不是文本節點
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新舊節點都存在子元素
        if (oldCh !== ch) 
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 只有新節點存在子元素,先清空節點上的文本,而後將子元素建立爲真實 DOM 插入
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 只有舊節點有子元素,直接刪除
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 清空舊節點上的文本
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新舊節點上的文本節點不一致,更新新節點上的 DOM
      nodeOps.setTextContent(elm, vnode.text)
    }
    //...
  }
複製代碼

patchVnode主要作了兩件事,函數

  • 一、判斷新節點是不是文本節點,若是是文本節點,就須要判斷與舊節點上的文本節點是否一致。不一致的時候,就須要更新節點上的文本。性能

  • 二、是當新節點不是文本節點時候,就須要對新舊節點的子元素進行判斷了。這裏有四種狀況:優化

    • 新舊節點都有children:使用updateChildren比較兩個children
    • 只有新節點有children:清空舊節點上的文本,而後將新節點建立爲真實DOM後,插入到父節點。
    • 只有舊節點有children:刪除節點上的children
    • 當只有舊節點上有文本時:新節點上沒有,直接清空便可。

updateChildren

重點看下updateChildren這個函數,spa

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

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 若是舊節點中開始的節點是 undefined,開始節點下標就後移一位
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 若是舊節點結束節點是 undefined,結束節點下標就遷移一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 舊開始節點與新開始節點相同,須要比較他們的子節點
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 以後舊開始節點、新開始節點,下標均後移一位
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 舊結束節點與新結束節點相同,須要比較他們的子節點
        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
        // 舊結束節點與新開始節點相同時,比較他們的子節點
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 將舊結束節點插入到舊開始節點以前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // 舊結束節點前移一位,新開始節點後移一位
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 不然,將每一箇舊節點的 key 值組成一個對應的 map 表,而後判斷新節點的 key 是否在 map 表中
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // idxInOld 是在舊節點列表中,與新節點相同的舊節點位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key] // key 值比較
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // sameVnode 進行比較
        if (isUndef(idxInOld)) { // New element
          // 若是 key 不在 map 表中,則建立新節點
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 若在,則判斷該 key 值對應的舊節點與新節點是不是相同的節點
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 若該 key 值對應的舊節點與新節點是相同的節點,則比較他們的子節點
            // 同時將該 key 值對應的節點插入到舊開始節點以前
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            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)
          }
        }
        // key 值判斷以後,新開始節點後移一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      // 若是舊節點列表先處理完,則將剩餘的新節點建立爲真實 DOM 插入
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 若是新節點列表先處理完,則刪除剩餘的舊節點
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
複製代碼

能夠看出updateChildren主要的做用是比較新舊子節點,分爲5種狀況:3d

  • 一、舊開始節點 == 新開始節點
    若舊開始節點與新開始節點相等時,說明舊開始節點的位置是對的,不須要更新該節點。以後是將舊開始節點和新開始節點的下標後移一位。

  • 二、舊結束節點 == 新結束節點
    若舊結束節點與新結束節點相等,說明舊節點的位置是對的,不須要更新該節點。以後是將舊結束節點和新結束節點的下標前移一位。

  • 三、舊開始節點 == 新結束節點
    若舊開始節點與新結束節點相等,說明舊開始節點的位置不對了,須要移動到oldEndVnode後面。而後將舊開始節點的下標後移一位,新結束節點的下標前移一位。

  • 四、舊結束節點 == 新開始節點
    若舊結束節點與新開始節點相等,說明舊結束節點須要移動到oldStartVnode前面。而後將舊結束節點前移一位,新開始節點位置後移一位。

  • 五、key 值查找code

    當前面四種比較都不行的時候,則會去經過key值進行查找。查找時候是當前的新節點,去遍歷舊節點數組,找到相同的舊節點,而後將其移到 oldStartVnode 前面。大體流程是:

處理剩餘節點

接着就是處理餘下的新舊節點。有兩種狀況:

  • 一、新節處理完了,舊節點還有剩餘
    將剩餘的舊節點,逐個刪除便可。
// 刪除剩餘的舊節點
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  
  function removeVnodes(vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
        } else { // Text node
          removeNode(ch.elm)
        }
      }
    }
  }
複製代碼
  • 二、新節點有剩餘,舊節點處理完了 逐個建立剩餘的新節點。有個問題是,將剩餘的新節點建立好後,插入到哪裏呢?
// 將剩餘的新節點建立爲真實的 DOM 插入
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }
複製代碼

咱們能夠看到,refElm 是獲取新節點最後一個節點。
若是refElm存在的話,說明最後一個節點以前被處理過,那麼剩餘的新節點插入到refElm前面便可。
若是refElm不存在,則將剩餘的新節點插入到父節點孩子的末尾。

本文到此也就結束了,相信你們也對 vue 中 diff 算法有必定了解。結束的結束,有個小問題,你們以爲 v-for 中 key值的做用是什麼呢?

相關文章
相關標籤/搜索