1. 程式人生 > 程式設計 >簡單談談Vue中的diff演算法

簡單談談Vue中的diff演算法

目錄
  • 概述
  • 虛擬Dom(virtual dom)
  • 原理
  • 實現過程
    • patch方法
    • sameVnode函式
    • patchVnode函式
    • updateChildren函式
  • 結語

    概述

    diff演算法,可以說是的一個比較核心的內容,之前只會用Vue來進行一些開發,具體的核心的內容其實涉獵不多,最近正好看了下這方面的內容,簡單聊下Vue2.0的diff演算法的實現吧,具體從幾個實現的函式來進行分析

    虛擬Dom(virtual dom)

    virtual DOhttp://www.cppcns.comM是將真實的DOM的資料抽取出來,以物件的形式模擬樹形結構

    比如以下是我們的真實DOM

    <div>
       <p>1234</p>
       <div>
           <span>1111</span>
       </div>
    </div>
    

    根據真實DOM生成的虛擬DOM如下

     var Vnode = {
         tag: 'div',children: [
             {
                 tag: 'p',text: '1234'
             },{
                 tag: 'div',children:[
                     {
                         tag: 'span',text: '1111'
                     }
                 ]
             }
         ]
     }
    

    原理

    diff的原理就是當前的真實的dom生成一顆virtual DOM也就是虛擬DOM,當虛擬DOM的某個節點的資料發生改變會生成一個新的Vnode,然後這個Vnode和舊的oldVnode對比,發現有不同,直接修改在真實DOM上

    實現過程

    diff演算法的實現過程核心的就是patch,其中的patchVnode,sameVnode以及updateChildren方法值得我們去關注一下,下面依次說明

    patch方法

    patch的核心邏輯是比較兩個Vnode節點,然後將差異更新到檢視上, 比對的方式是同級比較, 而不是每個層級的迴圈遍歷,如果比對之後得到差異,就將這些差異更新到檢視上,比對方式示例圖如下

    簡單談談Vue中的diff演算法

    sameVnode函式

    sameVnode的作用是判斷兩個節點是否相同,判斷相同的根據是key值,tag(標籤),isCommit(註釋),是否input的type一致等等,這種方法有點瑕疵,面對v-for下的key值使用index的情況,可能也會判斷是可複用節點。

    建議別使用index來作為key值。

    patchVnode函式

    //傳入幾個引數, oldVnode代表舊節點, vnode代表新節點, readOnly代表是否是隻讀節點
    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,insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
    
        //靜態樹的重用元素
    
    //如果vnode是克隆的,我們才會這樣做
    
    //如果新節點沒有被克隆,則表示呈現函式已經被克隆
    
    //通過hot-reload-api重置,我們需要做一個適當的重新渲染。
        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,removeOnly)
          } else if (isDef(ch)) {
            if (process.env.NODE_ENV !== 'production') {
              checkDuplicateKeys(ch)
            }
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm,'')
            addVnodes(elm,null,ch.length - 1,insertedVnodeQueue)
          } else if (isDef(oldCh)) {
            removeVnodes(oldCh,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)
        }
      }
    

    具體的實現邏輯是:

    1. 新舊節點一樣的時候,不需要做改變,直接返回
    2. 如果新舊都是靜態節點,並且具有相同的www.cppcns.comkey,當vnode是克隆節點或是v-once指令控制的節點時,只需要把oldVnode.elm和oldVnode.child都複製到vnode上
    3. 判斷vnode是否是註釋節點或者文字節點,從而做出以下處理
      1. 當vnode是文字節點或者註釋節點的時候,當vnode.text!== oldVnode.text的時候,只需要更新vnode的文字內容;
      2. oldVnode和vndoe都有子節點, 如果子節點不相同,就呼叫updateChildren方法,具體咋實現,下文有
      3. 如果只有vnode有子節點,判斷環境,如果不是生產環境,呼叫checkDuplicateKeys方法,判斷key值是否重複。之後在oldVnode上添加當前的ch
      4. 如果只有oldVnode上有子節點,那就呼叫方法刪除當前的節點

    updateChildren函式

    updateChildren,顧名思義,就是更新子節點的方法,從以上的patchVnode的方法,可以看出,當新舊節點都有子節點的時候,會執行這個方法。下面我們來了解下它的實現邏輯,也會有一些大家可能有看到過類似的示例圖,先看下程式碼

    function updateChildren (parentElm,newCh,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,newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode,newEndVnode)) {
            patchVnode(oldEndVnode,newEndVnode,newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode,newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode,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,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,oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              createElm(newStartVnode,parentElm,false,newStartIdx)
            } else {
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove,newStartVnode)) {
                patchVnode(vnodeToMove,newStartIdx)
                oldCh[idxInOld] = undefined
                canMove && nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode,newStartIdx)
              }
            }
            newStartVnohttp://www.cppcns.comde = newCh[++newStartIdx]
          }
        }
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm,refElm,newStartIdx,newEndIdx,insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh,oldEndIdx)
        }
      }
    

    在這裡我們先定義spFAqLbGXV幾個引數,oldStartIdx(舊節點首索引),oldEndIdx(舊節點尾索引),oldStartVnode(舊節點首元素), oldEndVnode(舊節點尾元素);同理,newStartIdx等四項即為新節點首索引等。

    看下while迴圈裡面的操作,也是核心內容

    在判斷是同一節點之後,節點也需要繼續進行patchVnode方法

    • 如果舊首元素和新首元素是相同節點,舊首索引和新首索引同時右移
    • 如果舊尾元素和新尾元素是相同節點,舊尾索引和新尾索引同時左移
    • 如果舊首元素點跟新尾元素是同一節點,根據方法上傳過來的readonly判斷,如果是false,那就把舊首元素移到舊節點的尾索引的後一位,同時舊首索引右移,新尾索引左移
    • 如果舊尾元素點跟新首元素是同一節點,根據方法上傳過來的readonly判斷,如果是false,那就把舊尾元素移到舊節點的首索引前一位,同時舊尾索引左移,新首索引右移
    • 如果以上都不符合
      判斷是否oldCh中有和newStartVnode的具有相同的key的Vnode,如果沒有找到,說明是新的節點,建立一個新的節點,插入即可

    如果找到了和newStartVnode具有相同的key的Vnode,命名為vnodeToMove,然後vnodeToMove和newStartVnode對比,如果相同,那就兩者再去patchVnode,如果removeOnly是false,則將找到的和newStartVnode具有相同的key的Vnode,叫vnodeToMove.elm,移動到oldStartVnode.elm之前
    如果key值相同,但是節點不相同,則建立一個新的節點

    在經過了While迴圈之後,如果發現新節點陣列或者舊節點數組裡面還有剩餘的節點,根據具體情況來進行刪除或者新增的操作

    當oldStartIdx > oldEndIdx的時候,表明,oldCh先遍歷完成,那就說明還有新的節點多餘,新增新的節點

    當newStartIdx > newEndIdx的時候,說明新節點最先遍歷完,舊節點還有剩餘,於是刪除剩餘的節點

    下面來看下示例圖

    原始節點(以oldVnode為舊節點, Vnode為新節點, diff為最後經過diff演算法之後生成的節點陣列)

    簡單談談Vue中的diff演算法

    迴圈第一次, 這裡我們發現舊尾元素跟新首元素一致,於是,舊尾元素D移動到舊首索引的前面,也就是在A的前面,同時,舊尾索引左移,新首索引右移

    迴圈第二次,新首元素和舊首元素一致,這時候兩元素位置不動,新舊首索引同時往右移動

    簡單談談Vue中的diff演算法

    迴圈第三次,發現舊元素裡發現沒有與當前元素相同的節點,於是新增,將F放在舊首元素之前,同理,第四次迴圈一致,兩次迴圈之後生成的新的示例圖

    簡單談談Vue中的diff演算法

    迴圈第五次,如同第二次迴圈

    簡單談談Vue中的diff演算法

    迴圈第六次,newStartIdx再次右移

    簡單談談Vue中的diff演算法

    7. 經過上次移動,newStartIdx > newEndIdx,已經退出while迴圈,證明那就是newCh先遍歷完成, oldCh還有多餘的節點,多餘的直接刪除,於是最後的出來的節點

    簡單談談Vue中的diff演算法

    以上就是幾個diff演算法相關的函式,以及diff演算法的實現過程

    結語

    diff演算法是虛擬DOM的核心一部分,同層比較,通過新老節點的對比,將改動的地方更新到真實DOM上。

    具體實現的方法是patch, patchVnode以及updateChildren

    patch的核心是,如果新節點有,舊節點沒有,新增; 舊節點有,新節點沒有, 刪除;如果都存在,判斷是否是相同,相同則呼叫patchVnode進行下一步比較

    patchVnode核心是:如果新舊節點不是註釋或者文字節點,新節點有子節點,而舊節點沒有子節點,則新增子節點;新節點沒有子節點,而舊節點有子節點,則刪除舊節點下的子節點;如果二者都有子節點,則呼叫updateChildren方法
    updateChildren的核心則是,新舊節點對比,進行新增,刪除或者更新。

    這裡只是初步的解釋了Vue2.0版本的diff演算法,其中的更加深層的原理以及Vue3.0的diff演算法有沒有什麼改變還有待學習。

    到此這篇關於Vue中diff演算法的文章就介紹到這了,更多相關Vue的diff演算法內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!