Virtual DOM

React的貢獻在于它提出的組件化、虛擬DOM、幫助整個前端進入工程化、將函數式編程的思想帶給前端。
這里記錄一下學習過程時關于虛擬DOM算法的實現。


一、Virtual DOM

在React中,render 執行的結果得到的并不是真正的 DOM 節點,結果僅僅是輕量級的 JavaScript 對象,我們稱之為virtual DOM。
真正的 DOM 元素屬性非常多,每次操作很有可能引起回流(Reflow)和重繪(Repaint)。
相對于真正的 DOM 對象,原生的 JavaScript 對象處理起來更快,而且更簡單。DOM 樹上的結構、屬性信息我們都可以很容易地用 JavaScript 對象表示出來:

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

對應的HTML:

<ul id='list'>
  <li class='item'>Item 1</li>
</ul>

所以可以用 JavaScript 對象表示的樹結構來構建一棵真正的DOM樹。用 JavaScript 對象表示 DOM 信息和結構,當狀態變更的時候,重新渲染這個 JavaScript 的對象結構。然后新渲染的對象樹去和舊的樹進行對比,記錄這兩棵樹差異。記錄下來的不同就是我們需要對頁面真正的 DOM 操作,然后把它們應用在真正的 DOM 樹上,頁面就變更了。
Virtual DOM 算法包括幾個步驟:

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

Virtual DOM 本質上就是在 JS 和 DOM 之間做了一個緩存。因為DOM 很慢,JS只操作Virtual DOM,最后的時候再把變更渲染到DOM。

二、算法實現

2.1. 用JS構建DOM樹
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'])
])

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節點,然后設置這個節點的屬性,最后遞歸地把自己的子節點也構建起來。

var ul = ul.render()
document.body.appendChild(ulRoot)

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

<ul id='list'>
  <li class='item'>Item 1</li>
</ul>

2.2. 比較兩棵DOM樹的差異
傳統 diff 算法的復雜度為 O(n3),React 通過制定大膽的策略:

  1. Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計。
  2. 兩個相同組件產生類似的DOM結構,不同的組件產生不同的DOM結構。
  • 對于同一層次的一組子節點,它們可以通過唯一的id進行區分。

將 O(n3) 復雜度的問題轉換成 O(n) 復雜度的問題。
通過diff策略,React 分別對 tree diff、component diff 以及 element diff 進行算法優化。

  1. tree diff
    基于策略一,React 對樹的算法進行了簡潔明了的優化,即對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
  2. component diff
  • 如果是同一類型的組件,按照原策略繼續比較 virtual DOM tree。
  • 如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節點。
  • 對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進行 diff。
  1. element diff
    當節點處于同一層級時,React diff 提供了三種節點操作,分別為:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。
  • INSERT_MARKUP:新的 component 類型不在老集合里, 即是全新的節點,需要對新節點執行插入操作。
  • MOVE_EXISTING:在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種情況下 prevChild=nextChild,就需要做移動操作,可以復用以前的 DOM 節點。
  • REMOVE_NODE:老 component 類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執行刪除操作,或者老 component 不在新集合里的,也需要執行刪除操作。
    React對其進行了優化,當節點相同時,只是由于位置發生變化。允許開發者對同一層級的同組子節點,添加唯一 key 進行區分。
    新老集合進行 diff 差異化對比,通過 key 發現新老集合中的節點都是相同的節點,因此無需進行節點刪除和創建,只需要將老集合中節點的位置進行移動,更新為新集合中節點的位置。

2.3把差異應用到真正的DOM樹上

具體可參考:
如何實現一個Virtual DOM算法
不可思議的react diff

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 參考文章:深度剖析:如何實現一個Virtual DOM 算法 作者:戴嘉華React中一個沒人能解釋清楚的問題——...
    waka閱讀 5,994評論 0 21
  • 了解過react的都必定會知道 virtual DOM 的存在,不夸張的說,virtual DOM 就是 reac...
    沐童Hankle閱讀 1,835評論 3 4
  • 一、讀懂diff diff是Unix/Linux系統的一個很重要的工具程序。它用來比較兩個文本文件的差異,是代碼版...
    overflow_hidden閱讀 1,891評論 2 2
  • 為什么我們需要UI框架? 響應式編程提出兩個最重要的觀點是:系統應該是事件驅動并且響應狀態的變化。DOM的UI組件...
    蘇星河閱讀 2,379評論 4 6
  • Virtual DOM是React中的一個很重要的概念,在日常開發中,前端工程師們需要將后臺的數據呈現到界面中,同...
    SherHoooo閱讀 1,005評論 3 5