學習vue源碼—vue-diff

本文主要記錄vue-diff的原理以及說明一個響應式數據更新的流程是怎麼樣的一個過程。vue

1. 數據改變到頁面渲染的過程是怎麼樣的?

首先看下面的圖片👇,這是執行click函數改變一個數據以後發生的函數調用棧,從圖上的說明能夠比較清楚個瞭解這個響應式過程的大概流程。下面簡單講解一下:node

  1. 改變數據,觸發這個被劫持過的數據的setter方法
  2. 執行這個數據的訂閱中心(dep)的notify方法
  3. update方法裏執行queueWatcher方法把watcher推入隊列
  4. 執行nextTick方法開始更新視圖
  5. run方法裏設置dep.target爲當前訂閱對象
  6. 調用get方法調用當前watchergetter執行更新方法
  7. updateComponent方法裏調用了render方法開始執行渲染頁面
  8. patchpatchVnodeupdateChildren方法都是比較VNode更新渲染的函數,不太重點的diff過程在updateChildren方法裏。

2. vue-diff的具體實現

patchVnodeupdateChildren方法在vue源碼項目的src/core/vdom/patch.js文件中。算法

先介紹patchVnode方法,這是執行真正更新dom的方法,大概的執行邏輯以下api

  1. 判斷vnode和oldVnode是否相等
  2. 判斷是否能重用vnode
  3. 判斷是否執行回調
  4. 判斷是否有children須要diff更新
  5. 判斷執行更新類型—新增dom、移除dom、更新textDom
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

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

接下來就是咱們常常說的vue-diff所在的方法updateChildren,先從參數提及,分別是父元素dom,舊的vnode-list,新的vnode-list,須要插入的vnode隊列,是否僅移除。bash

重點的邏輯在while循環裏:dom

如何理解這個diff邏輯,實際上是分別有新舊兩個vnode-list,兩個list都設定第一位和最後一位做爲兩個遊標,經過一系列判斷對比,不斷逼近,當兩個list的兩個遊標相交則循環結束。async

至於具體判斷的邏輯就不贅述了,代碼已經寫得很是清楚了,在這裏比較有意思的sameVnode的判斷,在使用v-for生成的vnode-list不設置key的時候,全部的對比更新幾乎都會從第三和第四個判斷分支進行,即代碼中的sameVnode(oldStartVnode, newStartVnode)sameVnode(oldEndVnode, newEndVnode)判斷,下面看看sameVnode的方法,當咱們不設置key的時候,判斷的邏輯會經過tag類型和vnode的數據某些屬性進行比較,一般來講都是相同的,這就是官方文檔說的原地複用邏輯,直接更新當前節點的內容,不須要對當前的節點進行移動。這對於節點內容相對簡單的來講默認會更高效,可是當節點內容相對複雜的時候咱們就須要對節點內容進行復用而不是從新生成,這時候咱們就須要設置key來複用節點。函數

最後的一段判斷oldStartIdx > oldEndIdxnewStartIdx > newEndIdx則說明符合這兩個條件的時候咱們當前vnode-list是從無到有或從有到無的變化。post

圖示:官方文檔的說明(👇)ui

sameVnode方法定義

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

updateChildren方法定義

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)
    }

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

總結

其實vue-diff的算法並不複雜,代碼閱讀起來也相對容易。在vue裏從patch到視圖的變化是實時的,即假如存在3個節點變化,vue並非收集完全部的patch再一次性更新視圖,而是在遍歷diff的過程當中patch直接更新視圖。

相關文章
相關標籤/搜索