關(guān)于一些Vue的文章。(4)

原文鏈接我的blog,歡迎STAR。

接著上一篇,我們繼續(xù)來講Vue的Virtual Dom diff 算法中的patchVnode方法,以及核心updateChildren方法。


在上篇中,我們談到,當(dāng)vnode不為真實(shí)節(jié)點(diǎn),且vnode與oldVnode為同一節(jié)點(diǎn)時(shí),會(huì)調(diào)用patchVnode方法。
我們直接從源碼上進(jìn)行分析:

  // patchVnode()有四個(gè)參數(shù)
  // oldVnode: 舊的虛擬節(jié)點(diǎn)
  // vnode: 新的虛擬節(jié)點(diǎn)
  // insertedVnodeQueue:  存在于整個(gè)patch中,用于收集patch中插入的vnode;
  // removeOnly: 這個(gè)在源碼里有提到,removeOnly is a special flag used only by<transition-group>也就是說是特殊的flag,用于transition-group組件。
  
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果oldVnode與vnode為同一引用, 不進(jìn)行任何處理。
    if (oldVnode === vnode) {
      return
    }
    
    // 如果不為同一引用,那說用新的vnode創(chuàng)建了。
    // 如果vnode, oldVnode都為靜態(tài)節(jié)點(diǎn),且vnode.key === oldVnode.key相等時(shí),當(dāng)vnode為克隆節(jié)點(diǎn),或者vnode有v-once指令時(shí),只需把oldVnode對(duì)應(yīng)的真實(shí)dom,以及組件實(shí)例都復(fù)制到vnode上。
    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

    // 在進(jìn)行下一步操作之前會(huì)調(diào)用prepatch hook,但是這個(gè)是vnode在data里定義的prepatch hook,并不是全局定義的prepatch hook
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    
    // 讓vnode引用到現(xiàn)在的真實(shí)DOM,當(dāng)elm修改的時(shí)候,會(huì)同步修改vnode.elm
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    
    // 我們先patchVnode, 方法就是先調(diào)用全局的update hook
    // 然后調(diào)用data里定義的update hook
    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)
    }
    
    // 如果vnode.text未定義
    // 這里有個(gè)值得注意的地方,具有text屬性的vnode不應(yīng)該具備有children
    // 對(duì)于<p>abc<i>123</i></p>的寫法應(yīng)該是
    // h('p', ['abc', h('i', '123')])
    // 而不是, h('p', 'abc', [h('i', '123')])
    // 因此,對(duì)text存在與否的情況需單獨(dú)拿出來分析
    if (isUndef(vnode.text)) {

      // 如果oldVnode與vnode都存在children
      if (isDef(oldCh) && isDef(ch)) {
        
        // 如果兩個(gè)children 不相同,調(diào)用updateChildren()方法更新子節(jié)點(diǎn)的操作。(接下來將講解)
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 如果只有vnode.children 存在
        // 當(dāng)oldVnode.text不為空,vnode.text未定義時(shí),清空elm.textContent
        // 添加vnode.children
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 如果只有oldVnode.children存在,移除oldVnode.children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 同上,如果oldVnode.text存在,vnode.text不存在,清空elm.textContent
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
        
      // 如果vnode.text存在(vnode是一個(gè)text node),且不等于oldVnode.text
      // 更新elm.textContent
      nodeOps.setTextContent(elm, vnode.text)
    }
    
    // 最后再調(diào)用 postpatch hook。
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

接著說重點(diǎn) 當(dāng)oldVnode.children與vnode.children都存在,且不相同時(shí)調(diào)用的updateChildren()方法, 同樣的,咱們從源碼上分析:

 // updateChildren(),有五個(gè)參數(shù)
 // parentElm: oldVnode.elm 的引用
 // oldCh, newCh: 分別是上面分析中的oldVnode.children, vnode.children
 // insertedVnodeQueue, removeOnly 請(qǐng)參考上面。
 
 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
    
    // 遍歷過程共有5種情況
    // 比較判斷的依據(jù)是,sameVnode(),值不得值得比較。
    // key,tag(當(dāng)前節(jié)點(diǎn)標(biāo)簽名),isComment(是否是注釋節(jié)點(diǎn))
    // data,節(jié)點(diǎn)的數(shù)據(jù)對(duì)象是否都存在或都不存在
    // (a, b)=> {
    //   return (
    //      a.key === b.key &&
    //      a.tag === b.tag &&
    //      a.isComment === b.isComment &&
    //      isDef(a.data) === isDef(b.data) &&
    //      sameInput(a, b)
    //  )
    // }
    // 當(dāng)oldStartIndex > oldEndIdx 或者 newStartIndex > newEndIdx, 停止遍歷。
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        
      // 對(duì)于vnode.key的比較,會(huì)把oldVnode = null
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]

        // 同上
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
      
        // 第一種情況:
        // 從oldCh與newCh的第一個(gè)開始,逐步往后遍歷。
        // 如果oldStartVnode與newStartVnode值得比較,
        // 執(zhí)行pathchVnode()方法
        // oldStartVnode, newStartVnode相對(duì)位置不變。
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        
        // 第二種情況:
        // 從oldCh與newCh的最后一個(gè)開始,逐步往前遍歷。
        // 如果oldEndVnode,newEndVnode值得比較
        // 執(zhí)行pathchVnode()
        // oldEndVnode, newEndVnode相對(duì)位置不變
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        
        // 第三種情況:
        // 從oldCh的第一個(gè),newCh的最后一個(gè)開始,oldCh往后,newCh往前遍歷,
        // 如果oldStartVnode與newEndVnode值得比較
        // 此時(shí)需要把oldStartVnode放到oldEndVnode后面
        // oldCh往后,newCh往前
        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)) {
        
        // 第四種情況:
        // 從oldCh的最后一個(gè),newCh的第一個(gè),oldCh往前,newCh往后,遍歷。
        // 如果oldEndVnode與newStartVnode值得比較
        // 此時(shí)需要把oldEndVnode放到oldStartVnode前邊
        // oldCh往前,newCh往后
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
      
        // 第五種情況:
        // 使用key比較
        // 首先會(huì)調(diào)用createKytoOldIdx()方法,產(chǎn)生一個(gè)key-index對(duì)象列表
        // 然后根據(jù)這個(gè)表來進(jìn)行更改
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        
        // 如果newStartVnode.key存在,根據(jù)key來找到對(duì)應(yīng)的index,命名為idxInOld 
        // 如果不存在,設(shè)置為null
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) {
        
          // 如果idxInOld不存在時(shí),此時(shí)是一個(gè)新的vnode
          // 將這個(gè)vnode插入到oldStartVnode.elm 的前邊
          // 把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
            
          // 如果idxInOld存在時(shí),那么對(duì)應(yīng)的oldVnode存在
          // 根據(jù)index,找到oldVnode對(duì)應(yīng)的children
          elmToMove = oldCh[idxInOld]

          // 如果不是生產(chǎn)環(huán)境,且elmToMove不存在
          // 此時(shí)因?yàn)閕dxInOld已經(jīng)存在,而oldCh[idxInOld]不存在
          // 只有可能keys重復(fù)了
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          
          // 如果根據(jù)vnode.key找出的elmToMove與newStartVnode值得比較比較
          // patchVnode這兩個(gè)節(jié)點(diǎn)
          // 之后,需要把這個(gè)child設(shè)置為undefined
          // 同時(shí)需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影響接下來的遍歷。
          if (sameVnode(elmToMove, newStartVnode)) {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]**重點(diǎn)內(nèi)容**
          } else {
            // same key but different element. treat as new element
            // 如果不值得比較,此時(shí)key已經(jīng)相同,說明是tag不同,或者其他不同,此時(shí)創(chuàng)建一個(gè)新節(jié)點(diǎn)
            // 將這個(gè)vnode插入到oldStartVnode.elm 的前邊
            // 把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    
    // 遍歷完成之后,存在兩種情況
    // 如果 oldStartIdx > oldEndIdx, 即oldCh先遍歷完
    // 位于 newStartIdx與newEndIdx之間的節(jié)點(diǎn)都可認(rèn)為是新的節(jié)點(diǎn)
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
        
      // 如果newStartIdx > newEndIdx, 即newCh先遍歷完
      // 此時(shí),位于oldStartIdx與oldEndIdx之間的節(jié)點(diǎn)已經(jīng)不存在了
      // 調(diào)用removeVnodes()方法移除節(jié)點(diǎn)。
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

直接在源碼上分析,可能有點(diǎn)亂,總結(jié)一下:

patchVnode共有以下情況:

  • 如果oldVnodevnode引用完全一致,則可以認(rèn)為沒有變化,無需進(jìn)行任何操作。

  • 如果vnode, oldVnode都為靜態(tài)節(jié)點(diǎn),且vnode.key === oldVnode.key相等時(shí),當(dāng)vnode為克隆節(jié)點(diǎn),或者vnodev-once指令時(shí),只需把oldVnode對(duì)應(yīng)的真實(shí)dom,以及組件實(shí)例都復(fù)制到vnode上。

  • 如果vnode不是text node:

    • 如果vnode.childrenoldVnode.children都存在,調(diào)用updateChildren()方法。

    • 當(dāng)vnode.children存在,oldVnode.children不存在時(shí),添加vnode.children。

    • 當(dāng)vnode.children不存在,oldVnode.children存在時(shí),需要移除oldVnode.children。

    • 當(dāng)兩者的children都不存在時(shí),如果oldVnodetext node,則需清空elm.textContent。

  • 如果vnodetext node,改變elm.textContent。

patchVnode有一個(gè)值得注意的地方是,vdom中規(guī)定,具有text屬性的vnode不應(yīng)該具備children,因此需把text node單獨(dú)拿出來分析。


updateChildren()方法共有5種比較方式,前四種無key的情況,后一種為有key的情況,當(dāng)oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的時(shí)候停止遍歷。

引用推薦的那篇文章圖:

遍歷示意圖
遍歷示意圖
  • 第一種比較方式從oldChnewCh各自第一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法進(jìn)行比較, 同時(shí)將oldChnewCh的下一個(gè)vnode分別設(shè)為oldStartVnodenewStartVnode(比較的相對(duì)位置不變), startVnode既是開始比較的vnode。

  • 第二種比較方式從oldChnewCh各自最后一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法進(jìn)行比較,同時(shí)將oldChnewCh的上一個(gè)vnode分別設(shè)置為oldEndVnodenewEndVnode(比較的相對(duì)位置不變),, endVnode既是結(jié)束比較的vnode

  • 第三種比較方式,從oldCh的第一個(gè)vnodenewCh的最后一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法比較,同時(shí)將oldCh的下一個(gè)vnode設(shè)置為oldStartVnode,將newCh的上一個(gè)vnode設(shè)置為newEndVnode,并且此時(shí)說明oldStartVnode.elm向右移動(dòng),并且已經(jīng)移動(dòng)到oldEndVnode.elm的后邊了,調(diào)用相應(yīng)的方法移動(dòng)位置。

  • 第四種比較方式,從oldCh的最后一個(gè)vnodenewCh的第一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法比較,同時(shí)將oldCh的上一個(gè)vnode設(shè)置為oldEndVnode,將newCh的上一個(gè)vnode設(shè)置為newStartVnode,并且此時(shí)說明oldEndVnode.elm向左移動(dòng),并且已經(jīng)移動(dòng)到oldStartVnode.elm的前邊了,調(diào)用相應(yīng)的方法移動(dòng)位置。

  • 第五種,使用key比較,先會(huì)產(chǎn)生一個(gè)key-index表,然后判斷vnode.key存在與否?

    • 如果不存在,是一個(gè)新的vnode,將這個(gè)vnode插入到oldStartVnode.elm 的前邊,并且把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)。

    • 如果存在,那么對(duì)應(yīng)的oldVnode應(yīng)該存在,此時(shí)可以根據(jù)key來找到對(duì)應(yīng)的vnode,然后判斷這個(gè)vnodenewStartVnode是否值得比較?

      • 當(dāng)值得比較時(shí),調(diào)用patchVnode,并且需要把這個(gè)child設(shè)置為undefined,同時(shí)需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影響接下來的遍歷。

      • 如果不值得比較,此時(shí)key已經(jīng)相同,說明是tag不同,或者其他不同,此時(shí)創(chuàng)建一個(gè)新節(jié)點(diǎn)將這個(gè)vnode插入到oldStartVnode.elm 的前邊。

遍歷完成后,如果oldCh先遍歷完,位于newStartIdx與newEndIdx之間的節(jié)點(diǎn)都可認(rèn)為是新的節(jié)點(diǎn),調(diào)用相應(yīng)的方法插入節(jié)點(diǎn)。如果newCh先遍歷完,此時(shí),位于oldStartIdx與oldEndIdx之間的節(jié)點(diǎn)已經(jīng)不存在了,調(diào)用removeVnodes()方法移除節(jié)點(diǎn)。


結(jié)語

碼完這篇?dú)v時(shí)四天的文章,我的身心是崩潰的,期間查閱了相當(dāng)多的資料,來確保表達(dá)的準(zhǔn)確性(當(dāng)然其中還有一些錯(cuò)誤的地方,如有發(fā)現(xiàn),請(qǐng)指出。),推薦從關(guān)于一些Vue的文章。(2),開始閱讀,算是逐漸深入吧,從render,template,el => vnode => diff算法。如果各位同學(xué)喜歡,麻煩點(diǎn)個(gè)贊。謝謝。

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容