成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

VirtualDOM與diff(Vue實(shí)現(xiàn))

MAX_zuo / 1842人閱讀

摘要:如果以上情況均不符合,則通過會得到一個(gè),里面存放了一個(gè)為舊的,為對應(yīng)序列的哈希表。從這個(gè)哈希表中可以找到是否有與一致的舊的節(jié)點(diǎn),如果同時(shí)滿足,的同時(shí)會將這個(gè)真實(shí)移動到對應(yīng)的真實(shí)的前面。

寫在前面

因?yàn)閷ue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。
文章的原地址:https://github.com/answershuto/learnVue。
在學(xué)習(xí)過程中,為Vue加上了中文的注釋https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對其他想學(xué)習(xí)Vue源碼的小伙伴有所幫助。
可能會有理解存在偏差的地方,歡迎提issue指出,共同學(xué)習(xí),共同進(jìn)步。

VNode

在刀耕火種的年代,我們需要在各個(gè)事件方法中直接操作DOM來達(dá)到修改視圖的目的。但是當(dāng)應(yīng)用一大就會變得難以維護(hù)。

那我們是不是可以把真實(shí)DOM樹抽象成一棵以JavaScript對象構(gòu)成的抽象樹,在修改抽象樹數(shù)據(jù)后將抽象樹轉(zhuǎn)化成真實(shí)DOM重繪到頁面上呢?于是虛擬DOM出現(xiàn)了,它是真實(shí)DOM的一層抽象,用屬性描述真實(shí)DOM的各個(gè)特性。當(dāng)它發(fā)生變化的時(shí)候,就會去修改視圖。

但是這樣的JavaScript操作DOM進(jìn)行重繪整個(gè)視圖層是相當(dāng)消耗性能的,我們是不是可以每次只更新它的修改呢?所以Vue.js將DOM抽象成一個(gè)以JavaScript對象為節(jié)點(diǎn)的虛擬DOM樹,以VNode節(jié)點(diǎn)模擬真實(shí)DOM,可以對這顆抽象樹進(jìn)行創(chuàng)建節(jié)點(diǎn)、刪除節(jié)點(diǎn)以及修改節(jié)點(diǎn)等操作,在這過程中都不需要操作真實(shí)DOM,只需要操作JavaScript對象,大大提升了性能。修改以后經(jīng)過diff算法得出一些需要修改的最小單位,再將這些小單位的視圖進(jìn)行更新。這樣做減少了很多不需要的DOM操作,大大提高了性能。

Vue就使用了這樣的抽象節(jié)點(diǎn)VNode,它是對真實(shí)Dom的一層抽象,而不依賴某個(gè)平臺,它可以是瀏覽器平臺,也可以是weex,甚至是node平臺也可以對這樣一棵抽象Dom樹進(jìn)行創(chuàng)建刪除修改等操作,這也為前后端同構(gòu)提供了可能。

具體VNode的細(xì)節(jié)可以看VNode節(jié)點(diǎn)。

修改視圖

周所周知,Vue通過數(shù)據(jù)綁定來修改視圖,當(dāng)某個(gè)數(shù)據(jù)被修改的時(shí)候,set方法會讓閉包中的Dep調(diào)用notify通知所有訂閱者Watcher,Watcher通過get方法執(zhí)行vm._update(vm._render(), hydrating)。

這里看一下_update方法

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    /*如果已經(jīng)該組件已經(jīng)掛載過了則代表進(jìn)入這個(gè)步驟是個(gè)更新的過程,觸發(fā)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__被用來作為一個(gè)入口*/
    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
    /*更新新的實(shí)例對象的__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方法的第一個(gè)參數(shù)是一個(gè)VNode對象,在內(nèi)部會將該VNode對象與之前舊的VNode對象進(jìn)行__patch__。

什么是__patch__呢?

patch

patch將新老VNode節(jié)點(diǎn)進(jìn)行比對,然后將根據(jù)兩者的比較結(jié)果進(jìn)行最小單位地修改視圖,而不是將整個(gè)視圖根據(jù)新的VNode重繪。patch的核心在于diff算法,這套算法可以高效地比較viturl dom的變更,得出變化以修改視圖。

那么patch如何工作的呢?

首先說一下patch的核心diff算法,diff算法是通過同層的樹節(jié)點(diǎn)進(jìn)行比較而非對樹進(jìn)行逐層搜索遍歷的方式,所以時(shí)間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。

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

讓我們看一下patch的代碼。

  /*createPatchFunction的返回值,一個(gè)patch函數(shù)*/
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    /*vnode不存在則直接調(diào)用銷毀鉤子*/
    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未定義的時(shí)候,其實(shí)也就是root節(jié)點(diǎn),創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /*標(biāo)記舊的VNode是否有nodeType*/
      /*Github:https://github.com/answershuto*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一個(gè)節(jié)點(diǎn)的時(shí)候直接修改現(xiàn)有的節(jié)點(diǎn)*/
        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)) {
            /*當(dāng)舊的VNode是服務(wù)端渲染的元素,hydrating記為true*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            /*需要合并到真實(shí)DOM上*/
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              /*調(diào)用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 " +
                "

, or missing . Bailing hydration and performing " + "full client-side render." ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it /*如果不是服務(wù)端渲染或者合并到真實(shí)DOM失敗,則創(chuàng)建一個(gè)空的VNode節(jié)點(diǎn)替換它*/ oldVnode = emptyNodeAt(oldVnode) } // replacing existing element /*取代現(xiàn)有元素*/ 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 /*組件根節(jié)點(diǎn)被替換,遍歷更新父節(jié)點(diǎn)element*/ let ancestor = vnode.parent while (ancestor) { ancestor.elm = vnode.elm ancestor = ancestor.parent } if (isPatchable(vnode)) { /*調(diào)用create回調(diào)*/ for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode.parent) } } } if (isDef(parentElm)) { /*移除老節(jié)點(diǎn)*/ removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { /*Github:https://github.com/answershuto*/ /*調(diào)用destroy鉤子*/ invokeDestroyHook(oldVnode) } } } /*調(diào)用insert鉤子*/ invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }

從代碼中不難發(fā)現(xiàn),當(dāng)oldVnode與vnode在sameVnode的時(shí)候才會進(jìn)行patchVnode,也就是新舊VNode節(jié)點(diǎn)判定為同一節(jié)點(diǎn)的時(shí)候才會進(jìn)行patchVnode這個(gè)過程,否則就是創(chuàng)建新的DOM,移除舊的DOM。

怎么樣的節(jié)點(diǎn)算sameVnode呢?

sameVnode

我們來看一下sameVnode的實(shí)現(xiàn)。

/*
  判斷兩個(gè)VNode節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),需要滿足以下條件
  key相同
  tag(當(dāng)前節(jié)點(diǎn)的標(biāo)簽名)相同
  isComment(是否為注釋節(jié)點(diǎn))相同
  是否data(當(dāng)前節(jié)點(diǎn)對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息)都有定義
  當(dāng)標(biāo)簽是的時(shí)候,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 
// so they need to be treated as different nodes
/*
  判斷當(dāng)標(biāo)簽是的時(shí)候,type是否相同
  某些瀏覽器不支持動態(tài)修改類型,所以他們被視為不同類型
*/
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
}

當(dāng)兩個(gè)VNode的tag、key、isComment都相同,并且同時(shí)定義或未定義data的時(shí)候,且如果標(biāo)簽為input則type必須相同。這時(shí)候這兩個(gè)VNode則算sameVnode,可以直接進(jìn)行patchVnode操作。

patchVnode

還是先來看一下patchVnode的代碼。

  /*patch VNode節(jié)點(diǎn)*/
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*兩個(gè)VNode節(jié)點(diǎn)相同則直接返回*/
    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都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)),
      并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記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)) {
      /*調(diào)用update回調(diào)以及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)
    }
    /*如果這個(gè)VNode節(jié)點(diǎn)沒有text文本時(shí)*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*新老節(jié)點(diǎn)均有children子節(jié)點(diǎn),則對子節(jié)點(diǎn)進(jìn)行diff操作,調(diào)用updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /*如果老節(jié)點(diǎn)沒有子節(jié)點(diǎn)而新節(jié)點(diǎn)存在子節(jié)點(diǎn),先清空elm的文本內(nèi)容,然后為當(dāng)前節(jié)點(diǎn)加入子節(jié)點(diǎn)*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "")
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /*當(dāng)新節(jié)點(diǎn)沒有子節(jié)點(diǎn)而老節(jié)點(diǎn)有子節(jié)點(diǎn)的時(shí)候,則移除所有ele的子節(jié)點(diǎn)*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /*當(dāng)新老節(jié)點(diǎn)都無子節(jié)點(diǎn)的時(shí)候,只是文本的替換,因?yàn)檫@個(gè)邏輯中新節(jié)點(diǎn)text不存在,所以直接去除ele的文本*/
        nodeOps.setTextContent(elm, "")
      }
    } else if (oldVnode.text !== vnode.text) {
      /*當(dāng)新老節(jié)點(diǎn)text不一樣時(shí),直接替換這段文本*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*調(diào)用postpatch鉤子*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patchVnode的規(guī)則是這樣的:

1.如果新舊VNode都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)),并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換elm以及componentInstance即可。

2.新老節(jié)點(diǎn)均有children子節(jié)點(diǎn),則對子節(jié)點(diǎn)進(jìn)行diff操作,調(diào)用updateChildren,這個(gè)updateChildren也是diff的核心。

3.如果老節(jié)點(diǎn)沒有子節(jié)點(diǎn)而新節(jié)點(diǎn)存在子節(jié)點(diǎn),先清空老節(jié)點(diǎn)DOM的文本內(nèi)容,然后為當(dāng)前DOM節(jié)點(diǎn)加入子節(jié)點(diǎn)。

4.當(dāng)新節(jié)點(diǎn)沒有子節(jié)點(diǎn)而老節(jié)點(diǎn)有子節(jié)點(diǎn)的時(shí)候,則移除該DOM節(jié)點(diǎn)的所有子節(jié)點(diǎn)。

5.當(dāng)新老節(jié)點(diǎn)都無子節(jié)點(diǎn)的時(shí)候,只是文本的替換。

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 
    // 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)) {
        /*前四種情況其實(shí)是指定key的時(shí)候,判定為同一個(gè)VNode,則直接patchVnode即可,分別比較oldCh以及newCh的兩頭節(jié)點(diǎn)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 {
        /*
          生成一個(gè)key與舊VNode的key對應(yīng)的哈希表(只有第一次進(jìn)來undefined的時(shí)候會生成,也為后面檢測重復(fù)的key值做鋪墊)
          比如childre是這樣的 [{xx: xx, key: "key0"}, {xx: xx, key: "key1"}, {xx: xx, key: "key2"}]  beginIdx = 0   endIdx = 2  
          結(jié)果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode節(jié)點(diǎn)存在key并且這個(gè)key在oldVnode中能找到則返回這個(gè)節(jié)點(diǎn)的idxInOld(即第幾個(gè)節(jié)點(diǎn),下標(biāo))*/
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode沒有key或者是該key沒有在老節(jié)點(diǎn)中找到則創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*獲取同key的老節(jié)點(diǎn)*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !elmToMove) {
            /*如果elmToMove不存在說明之前已經(jīng)有新節(jié)點(diǎn)放入過這個(gè)key的DOM中,提示可能存在重復(fù)的key,確保v-for的時(shí)候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的節(jié)點(diǎn)是同一個(gè)VNode則進(jìn)行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因?yàn)橐呀?jīng)patchVnode進(jìn)去了,所以將這個(gè)老節(jié)點(diǎn)賦值undefined,之后如果還有新節(jié)點(diǎn)與該節(jié)點(diǎn)key相同可以檢測出來提示已有重復(fù)的key*/
            oldCh[idxInOld] = undefined
            /*當(dāng)有標(biāo)識位canMove實(shí)可以直接插入oldStartVnode對應(yīng)的真實(shí)DOM節(jié)點(diǎn)前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時(shí)候(比如說tag不一樣或者是有不一樣type的input標(biāo)簽),創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /*全部比較完成以后,發(fā)現(xiàn)oldStartIdx > oldEndIdx的話,說明老節(jié)點(diǎn)已經(jīng)遍歷完了,新節(jié)點(diǎn)比老節(jié)點(diǎn)多,所以這時(shí)候多出來的新節(jié)點(diǎn)需要一個(gè)一個(gè)創(chuàng)建出來加入到真實(shí)DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比較完成以后發(fā)現(xiàn)newStartIdx > newEndIdx,則說明新節(jié)點(diǎn)已經(jīng)遍歷完了,老節(jié)點(diǎn)多余新節(jié)點(diǎn),這個(gè)時(shí)候需要將多余的老節(jié)點(diǎn)從真實(shí)DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

直接看源碼可能比較難以濾清其中的關(guān)系,我們通過圖來看一下。

首先,在新老兩個(gè)VNode節(jié)點(diǎn)的左右頭尾兩側(cè)都有一個(gè)變量標(biāo)記,在遍歷過程中這幾個(gè)變量都會向中間靠攏。當(dāng)oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時(shí)結(jié)束循環(huán)。

索引與VNode節(jié)點(diǎn)的對應(yīng)關(guān)系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode

在遍歷中,如果存在key,并且滿足sameVnode,會將該DOM節(jié)點(diǎn)進(jìn)行復(fù)用,否則則會創(chuàng)建一個(gè)新的DOM節(jié)點(diǎn)。

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

當(dāng)新老VNode節(jié)點(diǎn)的start或者end滿足sameVnode時(shí),也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節(jié)點(diǎn)進(jìn)行patchVnode即可。

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

這時(shí)候說明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,進(jìn)行patchVnode的同時(shí)還需要將真實(shí)DOM節(jié)點(diǎn)移動到oldEndVnode的后面。

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

這說明oldEndVnode跑到了oldStartVnode的前面,進(jìn)行patchVnode的同時(shí)真實(shí)的DOM節(jié)點(diǎn)移動到了oldStartVnode的前面。

如果以上情況均不符合,則通過createKeyToOldIdx會得到一個(gè)oldKeyToIdx,里面存放了一個(gè)key為舊的VNode,value為對應(yīng)index序列的哈希表。從這個(gè)哈希表中可以找到是否有與newStartVnode一致key的舊的VNode節(jié)點(diǎn),如果同時(shí)滿足sameVnode,patchVnode的同時(shí)會將這個(gè)真實(shí)DOM(elmToMove)移動到oldStartVnode對應(yīng)的真實(shí)DOM的前面。

當(dāng)然也有可能newStartVnode在舊的VNode節(jié)點(diǎn)找不到一致的key,或者是即便key相同卻不是sameVnode,這個(gè)時(shí)候會調(diào)用createElm創(chuàng)建一個(gè)新的DOM節(jié)點(diǎn)。

到這里循環(huán)已經(jīng)結(jié)束了,那么剩下我們還需要處理多余或者不夠的真實(shí)DOM節(jié)點(diǎn)。

1.當(dāng)結(jié)束時(shí)oldStartIdx > oldEndIdx,這個(gè)時(shí)候老的VNode節(jié)點(diǎn)已經(jīng)遍歷完了,但是新的節(jié)點(diǎn)還沒有。說明了新的VNode節(jié)點(diǎn)實(shí)際上比老的VNode節(jié)點(diǎn)多,也就是比真實(shí)DOM多,需要將剩下的(也就是新增的)VNode節(jié)點(diǎn)插入到真實(shí)DOM節(jié)點(diǎn)中去,此時(shí)調(diào)用addVnodes(批量調(diào)用createElm的接口將這些節(jié)點(diǎn)加入到真實(shí)DOM中去)。

2。同理,當(dāng)newStartIdx > newEndIdx時(shí),新的VNode節(jié)點(diǎn)已經(jīng)遍歷完了,但是老的節(jié)點(diǎn)還有剩余,說明真實(shí)DOM節(jié)點(diǎn)多余了,需要從文檔中刪除,這時(shí)候調(diào)用removeVnodes將這些多余的真實(shí)DOM刪除。

DOM操作

由于Vue使用了虛擬DOM,所以虛擬DOM可以在任何支持JavaScript語言的平臺上操作,譬如說目前Vue支持的瀏覽器平臺或是weex,在虛擬DOM的實(shí)現(xiàn)上是一致的。那么最后虛擬DOM如何映射到真實(shí)的DOM節(jié)點(diǎn)上呢?

Vue為平臺做了一層適配層,瀏覽器平臺見/platforms/web/runtime/node-ops.js以及weex平臺見/platforms/weex/runtime/node-ops.js。不同平臺之間通過適配層對外提供相同的接口,虛擬DOM進(jìn)行操作真實(shí)DOM節(jié)點(diǎn)的時(shí)候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實(shí)現(xiàn)則不需要關(guān)心,它會根據(jù)平臺的改變而改變。

現(xiàn)在又出現(xiàn)了一個(gè)問題,我們只是將虛擬DOM映射成了真實(shí)的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?

這要依賴于虛擬DOM的生命鉤子。虛擬DOM提供了如下的鉤子函數(shù),分別在不同的時(shí)期會進(jìn)行調(diào)用。

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

/*構(gòu)建cbs回調(diào)函數(shù),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]])
      }
    }
  }

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

以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節(jié)點(diǎn)均沒有attr屬性,則直接返回*/
  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
    return
  }
  let key, cur, old
  /*VNode節(jié)點(diǎn)對應(yīng)的Dom實(shí)例*/
  const elm = vnode.elm
  /*舊VNode節(jié)點(diǎn)的attr*/
  const oldAttrs = oldVnode.data.attrs || {}
  /*新VNode節(jié)點(diǎn)的attr*/
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  /*如果新的VNode的attr已經(jīng)有__ob__(代表已經(jīng)被Observe處理過了), 進(jìn)行深拷貝*/
  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)
      }
    }
  }
}

/*設(shè)置attr*/
function setAttr (el: Element, key: string, value: any) {
  if (isBooleanAttr(key)) {
    // set attribute for blank value
    // e.g. 
    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鉤子被調(diào)用時(shí)更新DOM的attr屬性即可。

關(guān)于

作者:染陌

Email:[email protected] or [email protected]

Github: https://github.com/answershuto

Blog:http://answershuto.github.io/

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

掘金: https://juejin.im/user/58f87ae844d9040069ca7507

osChina:https://my.oschina.net/u/3161824/blog

轉(zhuǎn)載請注明出處,謝謝。

歡迎關(guān)注我的公眾號

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/91913.html

相關(guān)文章

  • React && VUE Virtual Dom的Diff算法統(tǒng)一之路 snabbd

    摘要:毫無疑問的是算法的復(fù)雜度與效率是決定能夠帶來性能提升效果的關(guān)鍵因素。速度略有損失,但可讀性大大提高。因此目前的主流算法趨向一致,在主要思路上,與的方式基本相同。在里面實(shí)現(xiàn)了的算法與支持。是唯一添加的方法所以只發(fā)生在中。 VirtualDOM是react在組件化開發(fā)場景下,針對DOM重排重繪性能瓶頸作出的重要優(yōu)化方案,而他最具價(jià)值的核心功能是如何識別并保存新舊節(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu)之間差異的方法,...

    shixinzhang 評論0 收藏0
  • virtualDomDIFF算法關(guān)鍵過程整理

    摘要:,文本節(jié)點(diǎn)的比較,需要修改,則會調(diào)用。兩個(gè)節(jié)點(diǎn)都有子節(jié)點(diǎn),而且它們不一樣,這樣我們會調(diào)用函數(shù)比較子節(jié)點(diǎn),這是的核心。,新節(jié)點(diǎn)沒有子節(jié)點(diǎn),老節(jié)點(diǎn)有子節(jié)點(diǎn),直接刪除老節(jié)點(diǎn)。參考文章解析的算法 判斷對應(yīng)節(jié)點(diǎn)是否有必要進(jìn)行比較(sameVnode) function sameVnode(oldVnode, vnode){ return vnode.key === oldVnode.ke...

    hot_pot_Leo 評論0 收藏0
  • 用JavaScript自己寫MVVM前端框架-VM實(shí)現(xiàn)

    摘要:關(guān)于前端框架大家都有了解,或多或少的使用過,比如,,等等。那么你是否也想自己手寫一個(gè)的前端框架呢,我們從入手,手把手教你寫基于的前端框架,在整個(gè)編寫的過程中,希望大家學(xué)習(xí)更多,理解更多。本節(jié)我們以打包工具結(jié)合轉(zhuǎn)換插件實(shí)現(xiàn)數(shù)據(jù)的抽象。 關(guān)于MVVM前端框架大家都有了解,或多或少的使用過,比如Angular,React,VUE等等。那么你是否也想自己手寫一個(gè)MVVM的前端框架呢,我們從Vi...

    VincentFF 評論0 收藏0
  • React系列 --- virtualdom diff算法實(shí)現(xiàn)分析(三)

    摘要:所以只針對同層級節(jié)點(diǎn)做比較,將復(fù)雜度的問題轉(zhuǎn)換成復(fù)雜度的問題。 React系列 React系列 --- 簡單模擬語法(一)React系列 --- Jsx, 合成事件與Refs(二)React系列 --- virtualdom diff算法實(shí)現(xiàn)分析(三)React系列 --- 從Mixin到HOC再到HOOKS(四)React系列 --- createElement, ReactElem...

    sunsmell 評論0 收藏0
  • vue源碼閱讀之?dāng)?shù)據(jù)渲染過程

    摘要:圖在中應(yīng)用三數(shù)據(jù)渲染過程數(shù)據(jù)綁定實(shí)現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁面的過程,在中定義了一個(gè)的構(gòu)造函數(shù)。一、概述 vue已是目前國內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進(jìn)行總結(jié)。本文旨在梳理初始化頁面時(shí)data中的數(shù)據(jù)是如何渲染到頁面上的。本文將帶著這個(gè)疑問一點(diǎn)點(diǎn)追究vue的思路。總體來說vue模版渲染大致流程如圖1所...

    AlphaGooo 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<