首頁  >  文章  >  web前端  >  Vue中虛擬dom比較原理的介紹(範例講解)

Vue中虛擬dom比較原理的介紹(範例講解)

不言
不言轉載
2019-02-20 13:42:554014瀏覽

這篇文章帶給大家的內容是關於Vue中虛擬dom比較原理的介紹(範例講解),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

先說為什麼會有虛擬dom比較這個階段,我們知道了Vue是資料驅動視圖(資料的變化會造成視圖的變化),但你發現某個資料改變時,視圖是局部刷新而不是整個重新渲染,如何精準的找到資料對應的視圖並進行更新呢?那就需要拿到資料改變前後的dom結構,找到差異點並進行更新!

虛擬dom實質上是針對真實dom提煉出的簡單物件。就像一個簡單的p包含200多個屬性,但真正需要的可能只有tagName,所以對真實dom直接操作將大大影響效能!

Vue中虛擬dom比較原理的介紹(範例講解)

簡化後的虛擬節點(vnode)大致包含以下屬性:

{
  tag: 'p',       // 标签名
  data: {},         // 属性数据,包括class、style、event、props、attrs等
  children: [],     // 子节点数组,也是vnode结构
  text: undefined,  // 文本
  elm: undefined,   // 真实dom
  key: undefined    // 节点标识
}

虛擬dom的比較,就是找出新節點(vnode )和舊節點(oldVnode)之間的差異,然後對差異進行打補丁(patch)。大致流程如下

Vue中虛擬dom比較原理的介紹(範例講解)

整個過程還是比較簡單的,新舊節點如果不相似,直接根據新節點建立dom;如果相似,先是對data比較,包括class、style 、event、props、attrs等,有不同就呼叫對應的update函數,然後是子節點的比較,子節點的比較用到了diff演算法,這應該是這篇文章的重點和困難吧。

值得注意的是,在Children Compare 過程中,如果找到了相似的childVnode,那麼它們將遞歸進入新的打補丁過程。

原始碼解析

這次的原始碼解析寫簡潔一點,寫太多發現自己都不願意看(┬_┬)

開始

#先來看patch()函數:

function patch (oldVnode, vnode) {
  var elm, parent;
  if (sameVnode(oldVnode, vnode)) {
    // 相似就去打补丁(增删改)
    patchVnode(oldVnode, vnode);
  } else {
    // 不相似就整个覆盖
    elm = oldVnode.elm;
    parent = api.parentNode(elm);
    createElm(vnode);
    if (parent !== null) {
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }
  return vnode.elm;
}

patch()函數接收新舊vnode兩個參數,傳入的兩個參數有很大的差異:oldVnode的elm指向真實dom,而vnode的elm為undefined...但經過patch()方法後,vnode的elm 也會指向這個(更新過的)真實dom。

判斷新舊vnode是否相似的sameVnode()方法很簡單,就是比較tagkey#是否一致。

function sameVnode (a, b) {
  return a.key === b.key && a.tag === b.tag;
}

打補丁

對於新舊vnode不一致的處理方法很簡單,就是根據vnode建立真實dom,取代oldVnode中的elm插入DOM文檔。

對於新舊vnode一致的處理,就是我們前面常說到的打補丁了。具體什麼是打補丁?看看patchVnode()方法就知道了:

function patchVnode (oldVnode, vnode) {
  // 新节点引用旧节点的dom
  let elm = vnode.elm = oldVnode.elm;
  const oldCh = oldVnode.children;
  const ch = vnode.children;

  // 调用update钩子
  if (vnode.data) {
    updateAttrs(oldVnode, vnode);
    updateClass(oldVnode, vnode);
    updateEventListeners(oldVnode, vnode);
    updateProps(oldVnode, vnode);
    updateStyle(oldVnode, vnode);
  }

  // 判断是否为文本节点
  if (vnode.text == undefined) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) api.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      api.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    api.setTextContent(elm, vnode.text)
  }
}

打補丁其實就是呼叫各種updateXXX()函數,更新真實dom的各個屬性。每個的update函數都類似,就拿updateAttrs()舉例看看:

function updateAttrs (oldVnode, vnode) {
  let key, cur, old
  const elm = vnode.elm
  const oldAttrs = oldVnode.data.attrs || {}
  const attrs = vnode.data.attrs || {}

  // 更新/添加属性
  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if (old !== cur) {
      if (booleanAttrsDict[key] && cur == null) {
        elm.removeAttribute(key)
      } else {
        elm.setAttribute(key, cur)
      }
    }
  }
  // 删除新节点不存在的属性
  for (key in oldAttrs) {
    if (!(key in attrs)) {
      elm.removeAttribute(key)
    }
  }
}

屬性(Attribute)的更新函數的大致思路就是:

  • 遍歷vnode屬性,如果和oldVnode不一樣就呼叫setAttribute()修改;

  • 遍歷oldVnode屬性,如果不在vnode屬性中就呼叫removeAttribute()刪除。

你會發現裡面有個booleanAttrsDict[key]的判斷,是用來判斷在不在布林類型屬性字典中。

['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ......]

eg : <video autoplay></video>,想關閉自動播放,需要移除該屬性。

所有資料比較完後,就到子節點的比較了。先判斷目前vnode是否為文字節點,如果是文字節點就不用考慮子節點的比較;若是元素節點,就需要分三種情況考慮:

  • 新舊節點都有children ,那就進入子節點的比較(diff演算法);

  • 新節點有children,舊節點沒有,那就循環建立dom節點;

  • #新節點沒有children,舊節點有,那就循環刪除dom節點。

後面兩種情況都比較簡單,我們直接對第一種情況,子節點的比較進行分析。

diff演算法

子節點比較這部分程式碼比較多,先說說原理後面再貼程式碼。先看一張子節點比較的圖:

Vue中虛擬dom比較原理的介紹(範例講解)

圖中的oldChnewCh分別表示新舊子節點數組,它們都有自己的頭尾指標oldStartIdxoldEndIdxnewStartIdxnewEndIdx,陣列裡面儲存的是vnode,為了容易理解就用a,b,c,d等代替,它們表示不同類型標籤(p,span, p)的vnode物件。

子節點的比較實質上就是迴圈進行頭尾節點比較。循環結束的標誌是:舊子節點陣列或新子節點陣列遍歷完,(即 oldStartIdx > oldEndIdx || newStartIdx > newEndIdx)。大概看一下循環流程

  • #第一步 頭比較。若相似,舊頭新頭指標後移(即 oldStartIdx && newStartIdx ),真實dom不變,進入下一次循環;不相似,進入第二步。

  • 第二步 尾尾比較。若相似,舊尾新尾指標前移(即oldEndIdx-- && newEndIdx--),真實dom不變,進入下一次迴圈;不相似,進入第三步。

  • 第三步 頭尾比較。若相似,舊頭指標後移,新尾指標前移(即oldStartIdx && newEndIdx--),未確認dom序列中的頭移到尾,進入下一次循環;不相似,進入第四步。

  • 第四步 尾頭比較。若相似,舊尾指標前移,新頭指標後移(即oldEndIdx-- && newStartIdx ),未確認dom序列中的尾移到頭,進入下一次循環;不相似,進入第五步。

  • 第五步若節點有key且在舊子節點數組中找到sameVnode(tag和key都一致),則將其dom移動到當前真實dom序列的頭部,新頭指標後移(即newStartIdx );否則,vnode對應的dom(vnode[newStartIdx].elm)插入目前真實dom序列的頭部,新頭指標後移(即newStartIdx )。

先看看沒有key的情況,放個動圖看得更清楚些!

Vue中虛擬dom比較原理的介紹(範例講解)

相信看完圖片有更好的理解到diff演算法的精髓,整個過程還是比較簡單的。上圖中一共進入了6次循環,涉及了每一種情況,逐一敘述一下:

  • 第一次是頭頭相似(都是a), dom不改變,新舊頭指針均後移。 a節點確認後,真實dom序列為:a,b,c,d,e,f,未確認dom序列為:b,c,d,e,f

  • 第二次是尾尾相似(都是f),dom不改變,新舊尾指標都前移。 f節點確認後,真實dom序列為:a,b,c,d,e,f,未確認dom序列為:b,c,d,e ;

  • 第三次是頭尾相似(都是b),目前剩餘真實dom序列中的頭移到尾,舊頭指標後移,新尾指針前移。 b節點確認後,真實dom序列為:a,c,d,e,b,f,未確認dom序列為:c,d,e;

  • 第四次是尾頭相似(都是e),當前剩餘真實dom序列中的尾移到頭,舊尾指針前移,新頭指針後移。 e節點確認後,真實dom序列為:a,e,c,d,b,f,未確認dom序列為:c,d;

  • 第五次是均不相似,直接插入未確認dom序列頭部。 g節點插入後,真實dom序列為:a,e,g,c,d,b,f,未確認dom序列為:c,d;

  • 第六次是均不相似,直接插入未確認dom序列頭部。 h節點插入後,真實dom序列為:a,e,g,h,c,d,b,f,未確認dom序列為:c,d;

但結束循環後,有兩種情況需要考慮:

  • 新的字節點數組(newCh)被遍歷完(newStartIdx > newEndIdx)。那就需要把多餘的舊dom(oldStartIdx -> oldEndIdx)都刪除,上述例子就是c,d;

  • 新的字節點數組(oldCh)遍歷完(oldStartIdx > oldEndIdx)。那就需要把多餘的新dom(newStartIdx -> newEndIdx)都加進來。

上面说了这么多都是没有key的情况,说添加了:key可以优化v-for的性能,到底是怎么回事呢?因为v-for大部分情况下生成的都是相同tag的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样),那加了key会有什么帮助呢?这边引用一张图:

Vue中虛擬dom比較原理的介紹(範例講解)

key的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!

有兴趣的可以看看代码:

function updateChildren (parentElm, oldCh, newCh) {
  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, before

  while (oldStartIdx  oldEndIdx) {
    before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

以上是Vue中虛擬dom比較原理的介紹(範例講解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除