iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
0
Modern Web

技術在走,Vue.js 要有系列 第 10

|D10| 從原始碼看 Vue 元件化 (3) - patch

patch 主要是對新舊 vnode 進行 diff 比對,最後回傳所創建真正 的 DOM 節點完成畫面更新。

首先回顧一下,當 vm 實例需要更新時,會執行這段程式碼

// src/core/instance/lifecycle.js

vm._update(vm._render(), hydrating)

_update function 裡,vm.__patch__ 會調用 patch function

// src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...

    if (!prevVnode) {

      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false 
    } else {

      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    
    // ...
  }

// src/core/vdom/patch.js

  return function patch (oldVnode, vnode, hydrating, removeOnly) {

    /*************************************************
      當新 vnode 不存在且舊 vnode 存在時,return 舊 vnode,不執行 patch
    **************************************************/
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []

    /*************************************************
      若舊 vnode 不存在
    **************************************************/
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      /*************************************************
        就創建一個新 vnode
      **************************************************/
      createElm(vnode, insertedVnodeQueue)
    } else {

      /*************************************************
        取得舊 vnode 的 nodeType 来判斷是不是真正的 DOM
      **************************************************/
      const isRealElement = isDef(oldVnode.nodeType)
      
      /*************************************************
        如果不是真正的 DOM 且 `oldVnode` 和 `vnode` 是相同節點,就會進入 if 内部,執行`patchVnode`
      **************************************************/
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
      
       /*************************************************
         `patchVnode` 就是對新舊 vnode 進行 diff 來决定要如何更新
      **************************************************/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
          // ...
          oldVnode = emptyNodeAt(oldVnode)
        }
        // ...
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    
    /*************************************************
      最後 return 新 vnode 的節點內容
    **************************************************/
    return vnode.elm
  }

判斷 oldVnodevnode 是不是相同的節點透過 sameVnode

// src/core/vdom/patch.js

if (!isRealElement && sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} 

sameVnode 判斷條件主要有兩個

  • key 要相同
  • DOM 元素的 html tag 要相同,例如都是 div

都滿足以上條件,會認為是相同 vnode,就會執行 patchVnode

// 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)
      )
    )
  )
}
// src/core/vdom/patch.js

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    // ...

    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)
    }
    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)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

cbs 是從下面這段程式碼來的

// src/core/vdom/patch.js
// 
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

cbs.update 執行了下面幾個 call back,都是對當前 vnode 更新的各個階段執行對應的操作

  • updateAttributes
  • updateClass
  • updateDOMListeners
  • updateDOMProps
  • updateStyle
  • update
  • updateDirectives

更新完當前 vnode 後,就是對當前 vnode 的 children 做更新

// src/core/vdom/patch.js

    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 的 children 不是 textNode,再分成三種情況
    • 有新 children,沒有舊 children,創建新的
    • 沒有新 children,有舊 children,刪除舊的
    • 新 children、舊 children 都有,執行 updateChildren 比較 children 的差異,這裡就是 diff 算法的核心
  • 當前 vnode 的 children 是 textNode,直接更新 text

比較到最後都是調用 createElm 創建真正的 DOM,這裡分為兩種情況

  • 如果是元件,createComponent 會 return true,所以不會往下繼續,而會調用 $mount 來掛載元件
  • 如果不是元件,會透過遞迴對 children 創建節點
// src/core/vdom/patch.js

 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) 

上一篇
|D9| 從原始碼看 Vue 元件化 (2) - createComponent
下一篇
|D11| 從原始碼看 Vue 元件化 (4) - updateChildren,更新和移動子節點
系列文
技術在走,Vue.js 要有30

尚未有邦友留言

立即登入留言