學習Virtual Dom 筆記

實現虛擬(Virtual) Dom

把一個div元素的屬性打印出來,以下:javascript

能夠看到僅僅是第一層,真正 DOM的元素是很是龐大的,這也是 DOM加載慢的緣由。 相對於 DOM對象,原生的 JavaScript對象處理起來更快,並且更簡單。 DOM樹上的結構、屬性信息均可以用 JavaScript對象表示出來:

var element = {
  tagName: 'ul', // 節點標籤名
  props: { // DOM的屬性,用一個對象存儲鍵值對
    id: 'list'
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}
複製代碼

上面對應的HTML寫法是:前端

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>
複製代碼

DOM樹的信息能夠用JavaScript對象表示出來,則說明能夠用JavaScript對象去表示樹結構來構建一棵真正的DOM樹。java

狀態變動->從新渲染整個視圖的方式能夠用新渲染的對象樹去和舊的樹進行對比,記錄這兩棵樹的差別。二者的不一樣之處就是咱們須要對頁面真正的DOM操做,而後把它們應用在真正的DOM樹上,頁面就變動了。這樣能夠作到:視圖的結構確實是整個全新渲染了,可是最後操做DOM的只有變動不一樣的地方。node

Virtual DOM算法,能夠概括爲如下幾個步驟:

  1. 用JavaScript對象結構表示DOM樹的結構,而後用這個樹構建一個真正的DOM樹,插到文檔當中
  2. 當狀態變動的時候,從新構建一棵新的對象樹。而後用新的樹和舊的樹進行比較,記錄兩棵樹的差別
  3. 2所記錄的差別應用到步驟1所構建的的真正的DOM樹上,視圖就更新了

Virtual DOM本質就是在JS和DOM之間作了一個緩存,JS操做Virtual DOM,最後再應用到真正的DOM上。git

難點-算法實現

步驟一:用JS對象模擬虛擬DOMgithub

JavaScript來表示一個DOM節點,則須要記錄它的節點類型、屬性、子節點: element.js算法

function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}

module.exports = function (tagName, props, children) {
  return new Element(tagName, props, children)
}
複製代碼

上面的DOM結構能夠表示爲:數組

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])
複製代碼

如今ul只是一個JavaScript對象表示的DOM結構,頁面上並無這個結構。能夠根據這個ul構建真正的<ul>緩存

Element.prototype.render = function () {
  var el = document.createElement(this.tagName) // 根據tagName構建
  var props = this.props

  for (var propName in props) { // 設置節點的DOM屬性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點
      : document.createTextNode(child) // 若是字符串,只構建文本節點
    el.appendChild(childEl)
  })

  return el
}
複製代碼

render方法會根據tagName構建一個真正的DOM節點,而後設置這個節點的屬性,最後遞歸地把本身的子節點也構建起來。因此須要:app

var ulRoot = ul.render()
document.body.appendChild(ulRoot)
複製代碼

上面的ulRoot是真正的DOM節點,把它塞進文檔中,這樣body裏面就有了真正的<ul>的DOM結構:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>
複製代碼

步驟二:比較兩棵虛擬DOM樹的差別

比較兩棵DOM樹的差別是Virtual DOM算法最核心的部分,就是diff算法。兩棵樹的徹底diff算法是一個時間複雜度爲O(n^3)的問題。但在前端中,不多會跨越層級地移動DOM元素。因此Virtual DOM只會對同一層級的元素進行對比:

上面的 div只會和同一層級的 div對比,第二層級的只會跟第二層級對比。這樣算法複雜度就能夠達到 O(n)

a.深度優先遍歷,記錄差別 在實際的代碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每一個節點都會有一個惟一的標記:

在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的樹進行對比。若是有差別的話就記錄到一個對象裏面。

// diff 函數,對比兩棵樹
function diff (oldTree, newTree) {
  var index = 0 // 當前節點的標誌
  var patches = {} // 用來記錄每一個節點差別的對象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
  // 對比oldNode和newNode的不一樣,記錄下來
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍歷子節點
function diffChildren (oldChildren, newChildren, index, patches) {
  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
  })
}
複製代碼

例如,上面的div和新的div有差別,當前的標記是0,那麼:

patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不一樣
複製代碼

同理ppatches[1]ulpatches[3],以此類推

b.差別類型

DOM操做會有的差別:

  1. 替換掉原來的節點,例如把上面的div換成了section
  2. 移動、刪除、新增子節點,例如上面的div的子節點,把pul順序互換
  3. 修改了節點的屬性
  4. 對於文本節點,文本內容可能會改變。例如修改上面的文本節點2內容爲Virtual DOM2

因此定義了幾種差別類型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
複製代碼

對於節點的替換,判斷新舊節點的tagName和是否是同樣,若是不同就替換掉。如div換成section,記錄以下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]
複製代碼

若是給div新增了屬性idcontainer,記錄以下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}, {
  type: PROPS,
  props: {
    id: "container"
  }
}]
複製代碼

若是修改文本節點,如上面的文本節點2,記錄以下:

patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]
複製代碼

c.列表對比算法

上面若是把div中的子節點從新排序,看如puldiv的順序換成了divpul。按照同層進行順序對比的話,它們都會被替換掉,這樣DOM開銷很是大。而實際上只須要經過節點移動就能夠的了。 假設如今能夠英文字母惟一得標誌每個子節點: 舊的節點順序: a b c d e f g h i 如今對節點進行刪除、插入、移動的操做。新增j節點,刪除e節點,移動h節點: 新的節點順序: a b c h d f g i j 如今知道了新舊的順序,求最小的插入、刪除操做(移動能夠當作是刪除和插入操做的結合)。這個問題抽象出來實際上是字符串的最小編輯距離問題(Edition Distance),最多見的算法是Levenshtein Distance, 經過動態規劃求解,時間複雜度爲O(M*N)。而咱們只須要優化一些常見的移動操做,犧牲必定的DOM操做,讓算法時間複雜度達到線性的O((max(M,N)))。 獲取某個父節點的子節點的操做,就能夠記錄以下:

patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]
複製代碼

因爲tagName是能夠重複的,因此不能用這個來進行對比。須要給子節點加上一盒惟一標識key,列表對比的時候,使用key進行對比,這樣就能複用舊的DOM樹上的節點。 經過深度優先遍歷兩棵樹,每層節點進行對比,記錄下每一個節點的差別。完整的diff算法訪問:github.com/livoras/sim…

步驟三:把差別應用到真正的DOM樹上 由於步驟一所構建的JavaScript對象樹和render出來的真正的DOM樹的信息、結構是同樣的。因此能夠對那棵DOM樹也進行深度優先遍歷,遍歷的時候從步驟二生成的patches對象中找出當前遍歷的節點差別,而後進行DOM操做。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 從patches拿出當前節點的差別

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍歷子節點
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 對當前節點進行DOM操做
  }
}
複製代碼

applyPatches,根據不一樣類型的差別對當前節點進行 DOM操做:

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}
複製代碼

完整patch代碼訪問:github.com/livoras/sim…

歡迎關注
相關文章
相關標籤/搜索