通俗易懂的vue虛擬(Virtual )DOM和diff算法

最近在看一些底層方面的知識。因此想作個系列嘗試去聊聊這些比較複雜又很重要的知識點。學習就比如是座大山,只有本身去爬山,才能看到不同的風景,體會更加深入。今天咱們就來聊聊Vue中比較重要的vue虛擬(Virtual )DOM和diff算法。vue

虛擬(Virtual )DOM

Virtual DOM 其實就是一棵以 JavaScript 對象(VNode 節點)做爲基礎的樹,用對象屬性來描述節點,至關於在js和真實dom中間加來一個緩存,利用dom diff算法避免沒有必要的dom操做,從而提升性能。固然算法有時並非最優解,由於它須要兼容不少實際中可能發生的狀況,好比後續會講到兩個節點的dom樹移動。node

上幾篇文章中講vue的數據狀態管理結合Virtual DOM更容易理解,在vue中通常都是經過修改元素的state,訂閱者根據state的變化進行編譯渲染,底層的實現能夠簡單理解爲三個步驟:react

  • 一、用JavaScript對象結構表述dom樹的結構,而後用這個樹構建一個真正的dom樹,插到瀏覽器的頁面中。
  • 二、當狀態改變了,也就是咱們的state作出修改,vue便會從新構造一棵樹的對象樹,而後用這個新構建出來的樹和舊樹進行對比(只進行同層對比),記錄兩棵樹之間的差別。
  • 三、把2記錄的差別在從新應用到步驟1所構建的真正的dom樹,視圖就更新了。

舉例子:有一個 ul>li 列表,在template中的寫法是:git

<ul id='list'>
  <li class='item1'>Item 1</li>
  <li class='item2'>Item 2</li>
  <li class='item3' style='font-size: 20px'>Item 3</li>
</ul>
複製代碼

vue首先會將template進行編譯,這其中包括parse、optimize、generate三個過程。github

parse會使用正則等方式解析template模版中的指令、class、style等數據,造成AST,因而咱們的ul> li 可能被解析成下面這樣算法

// js模擬DOM結構
var element = {
  tagName: 'ul', // 節點標籤名
  props: { // DOM的屬性,用一個對象存儲鍵值對
    class: 'item',
    id: 'list'
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item1'}, children: "Item 1"},
    {tagName: 'li', props: {class: 'item2'}, children: "Item 2"},
    {tagName: 'li', props: {class: 'item3', style: 'font-size: 20px'}, children: "Item 3"},
  ]
}
複製代碼

optimize過程其實只是爲了優化後文diff算法的,若是不加這個過程,那麼每層的節點都須要作比對,即便沒變的部分也得弄一遍,這也違背了Virtual DOM 最初本質,形成沒必要要的資源計算和浪費。所以在編譯的過程當中vue會主動標記static靜態節點,我的理解爲就是頁面一些不變的或者不受state影響的節點。好比咱們的ul節點,不論li如何變化ul始終是不會變的,所以在這個編譯的過程當中能夠個ul打上一個標籤。當後續update更新視圖界面時,patch過程看到這個標籤會直接跳過這些靜態節點。數組

最後經過generate 將 AST 轉化成 render function 字符串,獲得結果是 render 的字符串以及 staticRenderFns 字符串。你們聽起來可能很困惑,首先前兩步你們應該都差很少知道了,當拿到一個AST時,vue內部有一個叫element ASTs的代碼生成器,猶如名字同樣generate函數拿到解析好的AST對象,遞歸AST樹,爲不一樣的AST節點建立不一樣的內部調用的方法,而後組合可執行的JavaScript字符串,等待後面的調用。最後可能會變成這個樣子:瀏覽器

function _render() {
  with (this) { 
    return __h__(
      'ul', 
      {staticClass: "list"}, 
      [
        " ",
        __h__('li', {class: item}, [String((msg))]),
        " ",
        __h__('li', {class: item}, [String((msg))]),
        "",
        __h__('li', {class: item}, [String((msg))]),
        ""
      ]
    )
  };
}
複製代碼

整個Virtual DOM生成的過程代碼中可簡化爲以下,有興趣的同窗能夠去看具體對應的Vue源碼,源碼位置在src/compiler/index.js緩存

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1.parse,模板字符串 轉換成 抽象語法樹(AST)
  const ast = parse(template.trim(), options)
  // 2.optimize,對 AST 進行靜態節點標記
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 3.generate,抽象語法樹(AST) 生成 render函數代碼字符串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
複製代碼

diff算法以及key的做用

在最初的diff算法實際上是"不可用的",由於時間複雜度是O(n^3)。假設一個dom樹有1000個節點,第一遍須要遍歷tree1,第二遍遍歷tree2,最後一遍就是排序組合成新樹。所以這1000個節點須要計算1000^3 = 1億次,這是很是龐大的計算,這種算法基本也不會用。bash

後面設計者們想出了一些方法,將時間複雜度由O(n^3)變成了O(n),那麼這些設計者是若是實現的?這也就是diff算法的優點所在,也是日常咱們所理解到一些知識:

  • 一、只比較同一級,不跨級比較
  • 二、tag不相同,直接刪掉重建,再也不深度比較
  • 三、tag和key,二者都相同,則認爲是相同節點,不在深度比較

這就是一個簡單的diff。經過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,因此時間複雜度只有 O(n)。

diff

以前在Virtual DOM中講到當狀態改變了,vue便會從新構造一棵樹的對象樹,而後用這個新構建出來的樹和舊樹進行對比。這個過程就是patch。比對得出「差別」,最終將這些「差別」更新到視圖上。patch的過程也是vue及react的核心算法,理解起來比較困難。先看一些簡單的圖形瞭解diff是如何比較新舊VNode的差別的。

  • 場景1:更新刪除移動

    diff移動
    移動的場景在diff中應該是最基礎的。要達到這樣的效果。咱們能夠將b移動到同層的最後面或者把c移動到B前面再把D也移動到B前面,固然這是在引入了key的比對結果。若是沒有key的話只會依次相互比較,將b ==> c、 c==> d、 d ==> b。而後在第三層中因爲新建的c沒有e、f所以會去新建e、f。爲了讓e、f獲得複用,設key後,會從用key生成的對象oldKeyToIdx中查找匹配的節點。讓算法知道不是刪除節點而是移動節點,這就是有key和無key的做用。在數組中插入新節點也是一樣的道理。

  • 場景2:刪除新建

    diff刪除新建
    咱們可能指望將C直接移動到B的後邊,這是最優的操做。可是實際的diff操做是移除c在建立一個c插入到b的下面,這就是同層比較的結果。若是在一些必要時能夠手工優化,例如在react的shouldComponentUpdate生命週期中就攔截了子組件的渲染進行優化。

在簡單的理解了diff算法實際操做的過程。爲了讓你們更好的掌握,由於這塊仍是比較複雜的。接下來將用僞代碼的形式分析diff算法是如何進行深度優先遍歷,記錄差別, Vue的VDOM的diff算法借鑑的是snabbdom,不妨先從snabbdom Example入手

在vue中首先會對新舊兩棵樹進行深度優先的遍歷,這樣每一個節點都會有一個惟一的標記。在遍歷的同時,每遍歷一個節點就會把該節點和新的樹進行對比,有差別的話就會記錄到一個對象裏。

/* 建立diff函數,接受新舊量兩棵參數 */
function diff (oldTree, newTree) {
  var index = 0 //當前節點的標誌
  var patches = {}  //用來記錄每一個節點差別的對象
  dfsWalk(oldTree, newTree, index, patches) // 對兩棵樹進行深度優先遍歷
  return patches //返回不一樣的記錄
}

function dfsWalk (oldNode, newNode, index, patches) {
  var currentPatch = []  // 定義一個數組將對比oldNode和newNode的不一樣,記錄下來
  if (newNode === null) {
    // 當執行從新排序時,真正的DOM節點將被刪除,所以不須要在這裏進行調整
  } else if (_.isString(oldNode) && _.isString(newNode)) {
    // 判斷oldNode、newNode是不是字符串對象或者字符串值
    if (newNode !== oldNode) {
        //節點不一樣直接放入到數組中
        currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 節點是相同的,diff區分舊節點的props和子節點 
    
    // diff處理props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    
    // diff處理子節點,若是有‘ignore’這個標誌的。diff就忽視這個子節點
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else {
    // 節點不相同,用新節點直接替換舊節點
     currentPatch.push({ type: patch.REPLACE, node: newNode })
  }
}
 function isIgnoreChildren (node) {
  return (node.props && node.props.hasOwnProperty('ignore'))
}

/* 處理子節點diffChildren函數 */
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  var diffs = listDiff(oldChildren, newChildren, 'key')
  newChildren = diffs.children

  if (diffs.moves.length) {
    var reorderPatch = { type: patch.REORDER, moves: diffs.moves }
    currentPatch.push(reorderPatch)
  }

 var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標識
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節點
    leftNode = child
  })
}
/* 處理子節點的props diffProps函數 */
function diffProps (oldNode, newNode) {
  var count = 0
  var oldProps = oldNode.props
  var newProps = newNode.props
  var key, value
  var propsPatches = {}
  // Find out different properties
  for (key in oldProps) {
    value = oldProps[key]
    if (newProps[key] !== value) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // Find out new property
  for (key in newProps) {
    value = newProps[key]
    if (!oldProps.hasOwnProperty(key)) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // If properties all are identical
  if (count === 0) {
    return null
  }
  return propsPatches
}
// 暴露diff函數
module.exports = diff

複製代碼

感興趣的話你也可查看簡化版的diff。 完整簡化版的diff算法

相關文章
相關標籤/搜索