Vue源碼學習3.8:組件更新&diff算法

建議PC端觀看,移動端代碼高亮錯亂html

1. 介紹

在組件化章節,咱們介紹了 Vue 的組件化實現過程,不過咱們只講了 Vue 組件的建立過程,並無涉及到組件數據發生變化,更新組件的過程。vue

而經過咱們這一章對數據響應式原理的分析,瞭解到當數據發生變化的時候,會觸發 渲染watcher 的回調函數,進而執行組件的更新過程。node

接下來咱們來詳細分析這一過程。web

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製代碼

組件的更新仍是調用了 vm._update 方法,咱們再回顧一下這個方法,它的定義在 src/core/instance/lifecycle.js 中:bash

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean{
  const vm: Component = this
  // ...
  const prevVnode = vm._vnode
  if (!prevVnode) {
     // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  // ...
}
複製代碼

組件更新的過程,會執行 vm.$el = vm.__patch__(prevVnode, vnode),它仍然會調用 patch 函數,在 src/core/vdom/patch.js 中定義:dom

vm.__patch__(prevVnode, vnode)

function patch (oldVnode, vnode, hydrating, removeOnly{
  // ...

  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // ...
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 新舊vnode相同的狀況
      patchVnode(oldVnode, vnode, insertedVnodeQueue, nullnull, removeOnly)
    } else {
      // 新舊vnode不一樣的狀況
      
      // 1. 建立新節點
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      createElm(
        vnode,
        insertedVnodeQueue,
        parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // 2. 遞歸更新父的佔位符節點
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          // 遍歷 cbs.destroy,依次destroy執行鉤子...
          
          ancestor.elm = vnode.elm
          
          // 是否可掛載
          if (patchable) {
            // 遍歷cbs.create,依次執行create鉤子...
          } else {
            registerRef(ancestor)
          }
          
          ancestor = ancestor.parent
        }
      }

      // 刪除舊節點
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 00)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  // ...
  return vnode.elm
}
複製代碼

這裏執行 patch 的邏輯和首次渲染是不同的,由於 oldVnode 不爲空,而且它和 vnode 都是 VNode 類型,接下來會經過 sameVNode(oldVnode, vnode) 判斷它們是不是相同的 VNode 來決定走不一樣的更新邏輯:異步

// src/core/vdom/patch.js
function sameVnode (a, b{
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

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)
}
複製代碼
  • 若是兩個 vnodekey 不相等,則確定是不一樣的
  • 不然繼續判斷對於同步組件,則判斷 isCommentdatainput 類型等是否相同
  • 對於異步組件,則判斷 asyncFactory 是否相同。

因此根據新舊 vnode 是否爲 sameVnode,會走到不一樣的更新邏輯,咱們先來講一下不一樣的狀況。async

2. 新舊vnode不一樣

其實在平常開發中基本上不會走到這個邏輯,只有當咱們這麼編寫組件時:編輯器

<template>
  <div v-if="flag">
  </div>
  <ul v-else>
    <li>1</li>
    <li>2</li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      flag: true
    }
  }
};
</script>
複製代碼

因爲咱們在最外層節點用了 v-if,因此會產生新舊節點不一樣的狀況,可是一般咱們都是在最外層用一個標籤包裹的。函數

下面回到 patch 函數,來看看新舊節點不一樣的狀況,這部分邏輯分爲三部分:

  • 建立新節點
  • 遞歸更新父的佔位符節點
  • 刪除舊節點

2.1 建立新節點

const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  nodeOps.nextSibling(oldElm)
)
複製代碼
  • 以舊 vnodeDOM 爲基礎得到父節點。
  • 調用 createElm:經過舊 vnode 建立真實的 DOM 並插入到它的父節點中。

2.2 遞歸更新父的佔位符節點

if (isDef(vnode.parent)) {
  let ancestor = vnode.parent
  const patchable = isPatchable(vnode)
  while (ancestor) {
    // 遍歷 cbs.destroy,依次destroy執行鉤子...

    ancestor.elm = vnode.elm

    // 是否可掛載
    if (patchable) {
      // 遍歷cbs.create,依次執行create鉤子...
    } else {
      registerRef(ancestor)
    }

    ancestor = ancestor.parent
  }
}
複製代碼

咱們只關注主要邏輯便可:獲取當前 渲染vnode父佔位符vnode (若是存在的話),先執行各個 moduledestroy 的鉤子函數,若是當前佔位符是一個可掛載的節點,則執行 modulecreate 鉤子函數。對於這些鉤子函數的做用,在以後的章節會詳細介紹。

一般狀況下 while 循環只執行一次:

// parent.vue
<template>
    <div>
        parent
        <Child></Child>
    </div>
</template>

// child.vue
<template>
    <div>child</div>
</template>
複製代碼

只有如下這種狀況 while 循環會屢次執行

// parent.vue
<template>
    <Child></Child>
</template>

// child.vue
<template>
    <div>child</div>
</template>
複製代碼

也就是說 父佔位符vnode 同時又是一個 渲染vnode 的狀況。

經過 isPatchable 函數用於判斷是否可掛載,源碼以下:

function isPatchable (vnode{
  // 存在 componentInstance 表示當前的渲染vnode,同時也是另外一個組件的佔位符vnode
  // 這種狀況則循環,直到找到最深層的組件
  while (vnode.componentInstance) {
    vnode = vnode.componentInstance._vnode
  }
  return isDef(vnode.tag)
}
複製代碼

2.3 刪除舊節點

if (isDef(parentElm)) {
  removeVnodes([oldVnode], 00)
else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}
複製代碼

oldVnode 從當前 DOM 樹中刪除,這其中執行 beforeDestroy & destroyed 兩個生命週期鉤子。

3. 新舊vnode相同

patch 函數中,當新舊 vnode 相同時:

if (!isRealElement && sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue, nullnull, removeOnly)
}
複製代碼

執行了 patchVnode 方法,參數咱們只關注 oldVnode & vnode 便可。

// src/core/vdom/patch.js
function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
{
  if (oldVnode === vnode) {
    return
  }

  const elm = vnode.elm = oldVnode.elm

  // ...

  // 執行 prepatch 鉤子函數
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  // 執行 update 鉤子函數
  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)
  }
  
  // patch 過程
  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)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      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) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  
  // 執行 postpatch 鉤子函數
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
複製代碼

patchVnode 函數的主要作了這幾件事:

  • 執行 prepatch 鉤子函數,這部分在下一章再結合 props 展開分析
  • 會執行全部 moduleupdate 鉤子函數以及用戶自定義的 update 鉤子函數
  • 核心 patch 過程
  • 執行 postpatch 鉤子函數

咱們本章重點來關注核心的 patch 過程

// patch 過程
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)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(ch)
    }
    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) {
  nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
  • 若是 vnode 不是文本節點,則判斷它們的子節點,並分了幾種狀況處理:

    • oldChch 都存在且不相同時,使用 updateChildren 函數來更新子節點,這個稍後重點講。
    • 若是隻有 ch 存在,表示舊節點不須要了。若是舊的節點是文本節點則先將節點的文本清除,而後經過 addVnodesch 批量插入到新節點 elm 下。
    • 若是隻有 oldCh 存在,表示更新的是空節點,則須要將舊的節點經過 removeVnodes 所有清除。
    • 當只有舊節點是文本節點的時候,則清除其節點文本內容。
  • 不然 vnode 是個文本節點且新舊文本不相同時,直接替換文本內容。

4. updateChildren

先貼出完整代碼,再結合圖片一步一步來看。

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly{
  let oldStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  
  let newStartIdx = 0
  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)
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]
    } 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 {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      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]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          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)
        }
      }
      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)
  }
}
複製代碼

3.1 變量介紹

開始以前定義了一系列的變量,分別以下:

  • oldStartIdxoldCh 的開始指針,對應的vnodeoldStartVnode
  • oldEndIdxoldCh 的結束指針,對應的 vnodeoldEndVnode
  • newStartIdxch 的開始指針,對應的 vnodenewStartVnode
  • newEndIdxch 的結束指針,對應的 vnodenewEndVnode
  • oldKeyToIdx 是一個 map,其中 key 就是常在 for 循環中寫的 key 的值,value 就是當前 vnode,也就是能夠經過惟一的 key,在 map 中找到對應的 vnode

3.2 循環條件

接下來是一個 while 循環,在這過程當中,oldStartIdxnewStartIdxoldEndIdx 以及 newEndIdx 會逐漸向中間靠攏。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
複製代碼

3.2 狀況一

首先當 oldStartVnode 或者 oldEndVnode 不存在的時候,oldStartIdxoldEndIdx 繼續向中間靠攏,並更新對應的 oldStartVnodeoldEndVnode 的指向

if (isUndef(oldStartVnode)) {
  oldStartVnode = oldCh[++oldStartIdx]
else if (isUndef(oldEndVnode)) {
  oldEndVnode = oldCh[--oldEndIdx]
}
複製代碼

接下來是將 oldStartVodenewStartVodeoldEndVode 以及 newEndVode 兩兩比對的過程,一共會出現 2*2=4 種狀況。

3.3 狀況二

首先是 oldStartVnodenewStartVnode 符合 sameVnode 時,直接進行 patchVnode,同時 oldStartIdxnewStartIdx 向後移動一位。

else if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
複製代碼

3.4 狀況三

其次是 oldEndVnodenewEndVnode 符合 sameVnode,一樣進行 patchVnode 操做並將 oldEndVnodenewEndVnode 向前移動一位。

else if (sameVnode(oldEndVnode, newEndVnode)) {
  patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  oldEndVnode = oldCh[--oldEndIdx]
  newEndVnode = newCh[--newEndIdx]
}
複製代碼

3.5 狀況四

先是 oldStartVnodenewEndVnode 符合 sameVnode 的時候,將 oldStartVnode.elm 這個節點直接移動到 oldEndVnode.elm 這個節點的後面便可。而後 oldStartIdx 向後移動一位,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]
}
複製代碼

3.6 狀況五

同理,oldEndVnodenewStartVnode 符合 sameVnode 時,將 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。一樣的,oldEndIdx 向前移動一位,newStartIdx 向後移動一位。

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]
}
複製代碼

3.7 狀況六

最後是當以上狀況都不符合的時候,這種狀況怎麼處理呢?

else {
  if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  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]
    if (sameVnode(vnodeToMove, newStartVnode)) {
      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)
    }
  }
  newStartVnode = newCh[++newStartIdx]
}
複製代碼

經過 createKeyToOldIdx 產生 keyindex 索引對應的一個 map 表:

function createKeyToOldIdx (children, beginIdx, endIdx{
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}
複製代碼

好比說:

[
    {xx: xx, key: 'key0'},
    {xx: xx, key: 'key1'}, 
    {xx: xx, key: 'key2'}
]
複製代碼

在通過 createKeyToOldIdx 轉化之後會變成:

{
    key0: 0, 
    key1: 1, 
    key2: 2
}
複製代碼

咱們能夠根據某一個 key 的值,快速地從 oldKeyToIdx 這個 map 中獲取相同 key 的節點的索引 idxInOld,而後找到相同的節點。

若是沒有 key 值則調用 findIdxInOldoldCh 找到相同 vnodefindIdxInOld 函數定義以下:

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
  }
}
複製代碼

若是沒有找到相同的節點,則經過 createElm 建立一個新節點,並將 newStartIdx 向後移動一位。

if (isUndef(idxInOld)) { // New element
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
複製代碼

不然若是找到了節點,同時它符合 sameVnode,則將這兩個節點進行 patchVnode,將該位置的老節點賦值 undefined(以後若是還有新節點與該節點key相同能夠檢測出來提示已有重複的 key ),同時將 vnodeToMove.elm 插入到 oldStartVnode.elm 的前面。同理,newStartIdx 日後移動一位。

若是不符合 sameVnode,只能建立一個新節點插入到 parentElm 的子節點中,newStartIdx 日後移動一位。

3.8 狀況七

最後一步就很容易啦,當 while 循環結束之後,若是 oldStartIdx > oldEndIdx,說明老節點比對完了,可是新節點還有多的,須要將新節點插入到真實 DOM 中去,調用 addVnodes 將這些節點插入便可。

同理,若是知足 newStartIdx > newEndIdx 條件,說明新節點比對完了,老節點還有多,將這些無用的老節點經過 removeVnodes 批量刪除便可。

相關文章
相關標籤/搜索