VirtualDOM與diff(Vue實現)

染陌同學發表於2017-09-18

寫在前面

因為對Vue.js很感興趣,而且平時工作的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js原始碼,並做了總結與輸出。
文章的原地址:github.com/answershuto…
在學習過程中,為Vue加上了中文的註釋github.com/answershuto…,希望可以對其他想學習Vue原始碼的小夥伴有所幫助。
可能會有理解存在偏差的地方,歡迎提issue指出,共同學習,共同進步。

VNode

在刀耕火種的年代,我們需要在各個事件方法中直接操作DOM來達到修改檢視的目的。但是當應用一大就會變得難以維護。

那我們是不是可以把真實DOM樹抽象成一棵以JavaScript物件構成的抽象樹,在修改抽象樹資料後將抽象樹轉化成真實DOM重繪到頁面上呢?於是虛擬DOM出現了,它是真實DOM的一層抽象,用屬性描述真實DOM的各個特性。當它發生變化的時候,就會去修改檢視。

但是這樣的JavaScript操作DOM進行重繪整個檢視層是相當消耗效能的,我們是不是可以每次只更新它的修改呢?所以Vue.js將DOM抽象成一個以JavaScript物件為節點的虛擬DOM樹,以VNode節點模擬真實DOM,可以對這顆抽象樹進行建立節點、刪除節點以及修改節點等操作,在這過程中都不需要操作真實DOM,只需要操作JavaScript物件,大大提升了效能。修改以後經過diff演算法得出一些需要修改的最小單位,再將這些小單位的檢視進行更新。這樣做減少了很多不需要的DOM操作,大大提高了效能。

Vue就使用了這樣的抽象節點VNode,它是對真實Dom的一層抽象,而不依賴某個平臺,它可以是瀏覽器平臺,也可以是weex,甚至是node平臺也可以對這樣一棵抽象Dom樹進行建立刪除修改等操作,這也為前後端同構提供了可能。

具體VNode的細節可以看VNode節點

修改檢視

周所周知,Vue通過資料繫結來修改檢視,當某個資料被修改的時候,set方法會讓閉包中的Dep呼叫notify通知所有訂閱者Watcher,Watcher通過get方法執行vm._update(vm._render(), hydrating)。

這裡看一下_update方法

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    /*如果已經該元件已經掛載過了則代表進入這個步驟是個更新的過程,觸發beforeUpdate鉤子*/
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    /*基於後端渲染Vue.prototype.__patch__被用來作為一個入口*/
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    /*更新新的例項物件的__vue__*/
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }複製程式碼

_update方法的第一個引數是一個VNode物件,在內部會將該VNode物件與之前舊的VNode物件進行patch

什麼是patch呢?

patch

patch將新老VNode節點進行比對,然後將根據兩者的比較結果進行最小單位地修改檢視,而不是將整個檢視根據新的VNode重繪。patch的核心在於diff演算法,這套演算法可以高效地比較viturl dom的變更,得出變化以修改檢視。

那麼patch如何工作的呢?

首先說一下patch的核心diff演算法,diff演算法是通過同層的樹節點進行比較而非對樹進行逐層搜尋遍歷的方式,所以時間複雜度只有O(n),是一種相當高效的演算法。

img
img

img
img

著兩張圖代表舊的VNode與新VNode進行patch的過程,他們只是在同層級的VNode之間進行比較得到變化(第二張圖中相同顏色的方塊代表互相進行比較的VNode節點),然後修改變化的檢視,所以十分高效。

讓我們看一下patch的程式碼。

  /*createPatchFunction的返回值,一個patch函式*/
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    /*vnode不存在則直接呼叫銷燬鉤子*/
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      /*oldVnode未定義的時候,其實也就是root節點,建立一個新的節點*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /*標記舊的VNode是否有nodeType*/
      /*Github:https://github.com/answershuto*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一個節點的時候直接修改現有的節點*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            /*當舊的VNode是服務端渲染的元素,hydrating記為true*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            /*需要合併到真實DOM上*/
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              /*呼叫insert鉤子*/
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          /*如果不是服務端渲染或者合併到真實DOM失敗,則建立一個空的VNode節點替換它*/
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        /*取代現有元素*/
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        if (isDef(vnode.parent)) {
          // component root element replaced.
          // update parent placeholder node element, recursively
          /*元件根節點被替換,遍歷更新父節點element*/
          let ancestor = vnode.parent
          while (ancestor) {
            ancestor.elm = vnode.elm
            ancestor = ancestor.parent
          }
          if (isPatchable(vnode)) {
            /*呼叫create回撥*/
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, vnode.parent)
            }
          }
        }

        if (isDef(parentElm)) {
          /*移除老節點*/
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          /*Github:https://github.com/answershuto*/
          /*呼叫destroy鉤子*/
          invokeDestroyHook(oldVnode)
        }
      }
    }

    /*呼叫insert鉤子*/
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }複製程式碼

從程式碼中不難發現,當oldVnode與vnode在sameVnode的時候才會進行patchVnode,也就是新舊VNode節點判定為同一節點的時候才會進行patchVnode這個過程,否則就是建立新的DOM,移除舊的DOM。

怎麼樣的節點算sameVnode呢?

sameVnode

我們來看一下sameVnode的實現。

/*
  判斷兩個VNode節點是否是同一個節點,需要滿足以下條件
  key相同
  tag(當前節點的標籤名)相同
  isComment(是否為註釋節點)相同
  是否data(當前節點對應的物件,包含了具體的一些資料資訊,是一個VNodeData型別,可以參考VNodeData型別中的資料資訊)都有定義
  當標籤是<input>的時候,type必須相同
*/
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)
  )
}

// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
/*
  判斷當標籤是<input>的時候,type是否相同
  某些瀏覽器不支援動態修改<input>型別,所以他們被視為不同型別
*/
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
}複製程式碼

當兩個VNode的tag、key、isComment都相同,並且同時定義或未定義data的時候,且如果標籤為input則type必須相同。這時候這兩個VNode則算sameVnode,可以直接進行patchVnode操作。

patchVnode

還是先來看一下patchVnode的程式碼。

  /*patch VNode節點*/
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*兩個VNode節點相同則直接返回*/
    if (oldVnode === vnode) {
      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.
    /*
      如果新舊VNode都是靜態的,同時它們的key相同(代表同一節點),
      並且新的VNode是clone或者是標記了once(標記v-once屬性,只渲染一次),
      那麼只需要替換elm以及componentInstance即可。
    */
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      /*i = data.hook.prepatch,如果存在的話,見"./create-component componentVNodeHooks"。*/
      i(oldVnode, vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      /*呼叫update回撥以及update鉤子*/
      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)
    }
    /*如果這個VNode節點沒有text文字時*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*新老節點均有children子節點,則對子節點進行diff操作,呼叫updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /*如果老節點沒有子節點而新節點存在子節點,先清空elm的文字內容,然後為當前節點加入子節點*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /*當新節點沒有子節點而老節點有子節點的時候,則移除所有ele的子節點*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /*當新老節點都無子節點的時候,只是文字的替換,因為這個邏輯中新節點text不存在,所以直接去除ele的文字*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      /*當新老節點text不一樣時,直接替換這段文字*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*呼叫postpatch鉤子*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }複製程式碼

patchVnode的規則是這樣的:

1.如果新舊VNode都是靜態的,同時它們的key相同(代表同一節點),並且新的VNode是clone或者是標記了once(標記v-once屬性,只渲染一次),那麼只需要替換elm以及componentInstance即可。

2.新老節點均有children子節點,則對子節點進行diff操作,呼叫updateChildren,這個updateChildren也是diff的核心。

3.如果老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文字內容,然後為當前DOM節點加入子節點。

4.當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的所有子節點。

5.當新老節點都無子節點的時候,只是文字的替換。

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, elmToMove, 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

    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)) {
        /*前四種情況其實是指定key的時候,判定為同一個VNode,則直接patchVnode即可,分別比較oldCh以及newCh的兩頭節點2*2=4種情況*/
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        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)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          生成一個key與舊VNode的key對應的雜湊表(只有第一次進來undefined的時候會生成,也為後面檢測重複的key值做鋪墊)
          比如childre是這樣的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          結果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode節點存在key並且這個key在oldVnode中能找到則返回這個節點的idxInOld(即第幾個節點,下標)*/
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode沒有key或者是該key沒有在老節點中找到則建立一個新的節點*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*獲取同key的老節點*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            /*如果elmToMove不存在說明之前已經有新節點放入過這個key的DOM中,提示可能存在重複的key,確保v-for的時候item有唯一的key值*/
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            /*Github:https://github.com/answershuto*/
            /*如果新VNode與得到的有相同key的節點是同一個VNode則進行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因為已經patchVnode進去了,所以將這個老節點賦值undefined,之後如果還有新節點與該節點key相同可以檢測出來提示已有重複的key*/
            oldCh[idxInOld] = undefined
            /*當有標識位canMove實可以直接插入oldStartVnode對應的真實DOM節點前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*當新的VNode與找到的同樣key的VNode不是sameVNode的時候(比如說tag不一樣或者是有不一樣type的input標籤),建立一個新的節點*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /*全部比較完成以後,發現oldStartIdx > oldEndIdx的話,說明老節點已經遍歷完了,新節點比老節點多,所以這時候多出來的新節點需要一個一個建立出來加入到真實DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比較完成以後發現newStartIdx > newEndIdx,則說明新節點已經遍歷完了,老節點多餘新節點,這個時候需要將多餘的老節點從真實DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }複製程式碼

直接看原始碼可能比較難以濾清其中的關係,我們通過圖來看一下。

img
img

首先,在新老兩個VNode節點的左右頭尾兩側都有一個變數標記,在遍歷過程中這幾個變數都會向中間靠攏。當oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時結束迴圈。

索引與VNode節點的對應關係:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode

在遍歷中,如果存在key,並且滿足sameVnode,會將該DOM節點進行復用,否則則會建立一個新的DOM節點。

首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2*2=4種比較方法。

當新老VNode節點的start或者end滿足sameVnode時,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節點進行patchVnode即可。

img
img

如果oldStartVnode與newEndVnode滿足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。

這時候說明oldStartVnode已經跑到了oldEndVnode後面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的後面。

img
img

如果oldEndVnode與newStartVnode滿足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。

這說明oldEndVnode跑到了oldStartVnode的前面,進行patchVnode的同時真實的DOM節點移動到了oldStartVnode的前面。

img
img

如果以上情況均不符合,則通過createKeyToOldIdx會得到一個oldKeyToIdx,裡面存放了一個key為舊的VNode,value為對應index序列的雜湊表。從這個雜湊表中可以找到是否有與newStartVnode一致key的舊的VNode節點,如果同時滿足sameVnode,patchVnode的同時會將這個真實DOM(elmToMove)移動到oldStartVnode對應的真實DOM的前面。

img
img

當然也有可能newStartVnode在舊的VNode節點找不到一致的key,或者是即便key相同卻不是sameVnode,這個時候會呼叫createElm建立一個新的DOM節點。

img
img

到這裡迴圈已經結束了,那麼剩下我們還需要處理多餘或者不夠的真實DOM節點。

1.當結束時oldStartIdx > oldEndIdx,這個時候老的VNode節點已經遍歷完了,但是新的節點還沒有。說明了新的VNode節點實際上比老的VNode節點多,也就是比真實DOM多,需要將剩下的(也就是新增的)VNode節點插入到真實DOM節點中去,此時呼叫addVnodes(批量呼叫createElm的介面將這些節點加入到真實DOM中去)。

img
img

2。同理,當newStartIdx > newEndIdx時,新的VNode節點已經遍歷完了,但是老的節點還有剩餘,說明真實DOM節點多餘了,需要從文件中刪除,這時候呼叫removeVnodes將這些多餘的真實DOM刪除。

img
img

DOM操作

由於Vue使用了虛擬DOM,所以虛擬DOM可以在任何支援JavaScript語言的平臺上操作,譬如說目前Vue支援的瀏覽器平臺或是weex,在虛擬DOM的實現上是一致的。那麼最後虛擬DOM如何對映到真實的DOM節點上呢?

Vue為平臺做了一層適配層,瀏覽器平臺見/platforms/web/runtime/node-ops.js以及weex平臺見/platforms/weex/runtime/node-ops.js。不同平臺之間通過適配層對外提供相同的介面,虛擬DOM進行操作真實DOM節點的時候,只需要呼叫這些適配層的介面即可,而內部實現則不需要關心,它會根據平臺的改變而改變。

現在又出現了一個問題,我們只是將虛擬DOM對映成了真實的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?

這要依賴於虛擬DOM的生命鉤子。虛擬DOM提供瞭如下的鉤子函式,分別在不同的時期會進行呼叫。

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

/*構建cbs回撥函式,web平臺上見/platforms/web/runtime/modules*/
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }複製程式碼

同理,也會根據不同平臺有自己不同的實現,我們這裡以Web平臺為例。Web平臺的鉤子函式見/platforms/web/runtime/modules。裡面有對attr、class、props、events、style以及transition(過渡狀態)的DOM屬性進行操作。

以attr為例,程式碼很簡單。

/* @flow */

import { isIE9 } from 'core/util/env'

import {
  extend,
  isDef,
  isUndef
} from 'shared/util'

import {
  isXlink,
  xlinkNS,
  getXlinkProp,
  isBooleanAttr,
  isEnumeratedAttr,
  isFalsyAttrValue
} from 'web/util/index'

/*更新attr*/
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  /*如果舊的以及新的VNode節點均沒有attr屬性,則直接返回*/
  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
    return
  }
  let key, cur, old
  /*VNode節點對應的Dom例項*/
  const elm = vnode.elm
  /*舊VNode節點的attr*/
  const oldAttrs = oldVnode.data.attrs || {}
  /*新VNode節點的attr*/
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  /*如果新的VNode的attr已經有__ob__(代表已經被Observe處理過了), 進行深拷貝*/
  if (isDef(attrs.__ob__)) {
    attrs = vnode.data.attrs = extend({}, attrs)
  }

  /*遍歷attr,不一致則替換*/
  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if (old !== cur) {
      setAttr(elm, key, cur)
    }
  }
  // #4391: in IE9, setting type can reset value for input[type=radio]
  /* istanbul ignore if */
  if (isIE9 && attrs.value !== oldAttrs.value) {
    setAttr(elm, 'value', attrs.value)
  }
  for (key in oldAttrs) {
    if (isUndef(attrs[key])) {
      if (isXlink(key)) {
        elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
      } else if (!isEnumeratedAttr(key)) {
        elm.removeAttribute(key)
      }
    }
  }
}

/*設定attr*/
function setAttr (el: Element, key: string, value: any) {
  if (isBooleanAttr(key)) {
    // set attribute for blank value
    // e.g. <option disabled>Select one</option>
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, key)
    }
  } else if (isEnumeratedAttr(key)) {
    el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true')
  } else if (isXlink(key)) {
    if (isFalsyAttrValue(value)) {
      el.removeAttributeNS(xlinkNS, getXlinkProp(key))
    } else {
      el.setAttributeNS(xlinkNS, key, value)
    }
  } else {
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, value)
    }
  }
}

export default {
  create: updateAttrs,
  update: updateAttrs
}複製程式碼

attr只需要在create以及update鉤子被呼叫時更新DOM的attr屬性即可。

關於

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: github.com/answershuto

Blog:answershuto.github.io/

知乎專欄:zhuanlan.zhihu.com/ranmo

掘金: juejin.im/user/58f87a…

osChina:my.oschina.net/u/3161824/b…

轉載請註明出處,謝謝。

歡迎關注我的公眾號

相關文章