Vue 源碼中 的 diff 以及 對 key 的使用,以及和 react 之間的區別

一直不想寫 vue diff 的過程,因爲 這一塊網絡上講的人實在是太多了,這裏寫了也只不過是拾人牙慧罷了,但是不寫吧,又覺得心癢癢的,畢竟前面寫了一篇 react 源碼中 對 key 的使用 的

首先是幾個工具函數

// 這裏用來判斷 一個值是否是 未定義的
export function isDef(v: any): boolean % checks {
  return v !== undefined && v !== null
}
// 對比 是不是 新節點
// 這裏和之前的區別是, 沒有設置 key 的情況下,key 被認爲是相等的
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    if (isDef(c) && sameVnode(node, c)) return i
  }
}
// 兩個 vnode 是否相同
function sameVnode(a, b) {
  return (
    // 需要注意的是 null === null 爲 true
    // 也就是說,如果 沒有設置 key 的話,那麼就默認爲 兩個節點的key 是一致的
    // 對比於 react 的diff ,這裏的判斷 多出了 tag、comment 和 input 的判斷
    // 而 react 則着重於 $$type 的判斷
    // 這裏 react 明顯的 對於 jsx 的分類 更傾向於 自己的判斷,而 vue 則是 傾向於 用戶的輸入
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        // 是否是 同一個類型的 input 標籤,這個很好理解
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
// 是否是 相同類型的 input 節點,比如 radio,text,checkbox等
function sameInputType(a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

真正 diff 的過程

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)
    }
    // 而這裏,也是和 react diff 的最大的不同之處了,
    // react 對於新舊節點只做了順序的 查找 相同節點,而 vue 還做了 前後節點的 對比
    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, 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 和 位置 做一個 對應關係
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 查找 在 老節點的 key 對應 map 中有沒有 可以複用的節點
        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]
          // 然後再對比 input 的type 和 tag 等屬性,一樣的話,吧老節點 對應的內容制空,複用節點
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 雖然 key 相等,但是節點類型不同,創建新節點
            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(oldCh, oldStartIdx, oldEndIdx)
    }
  }

所以 react 和 vue 的 diff 之間的異同?

  1. 當 key 爲 null,也就是沒有設置 key 的時候,兩個都是當作 相同的節點來進行的

  2. react 和 vue 中對應 兩個節點是否 可以複用 有着不同的判斷

  3. react 中的 複用,指的是 key 相等,然後 type 相等,這裏的 type 是在創建的時候,根據節點類型 設置的 

  4. 而 vue 的 複用,指的是 key 相等,節點的 tag 類型相等,input 的 type 相等

  5. 而 vue diff 中的 優化,指的就是 對 排頭排尾 的比較了,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章