深度剖析:如何實(shí)現(xiàn)一個(gè) Virtual DOM 算法

目錄:

  • 1 前言
  • 2 對(duì)前端應(yīng)用狀態(tài)管理思考
  • 3 Virtual DOM 算法
  • 4 算法實(shí)現(xiàn)
    • 4.1 步驟一:用JS對(duì)象模擬DOM樹(shù)
    • 4.2 步驟二:比較兩棵虛擬DOM樹(shù)的差異
    • 4.3 步驟三:把差異應(yīng)用到真正的DOM樹(shù)上
  • 5 結(jié)語(yǔ)
  • 6 References

1 前言

本文會(huì)在教你怎么用 300~400 行代碼實(shí)現(xiàn)一個(gè)基本的 Virtual DOM 算法援岩,并且嘗試盡量把 Virtual DOM 的算法思路闡述清楚秕铛。希望在閱讀本文后跃捣,能讓你深入理解 Virtual DOM 算法,給你現(xiàn)有前端的編程提供一些新的思考摧冀。

本文所實(shí)現(xiàn)的完整代碼存放在

Github谊路。

2 對(duì)前端應(yīng)用狀態(tài)管理的思考

假如現(xiàn)在你需要寫(xiě)一個(gè)像下面一樣的表格的應(yīng)用程序盗忱,這個(gè)表格可以根據(jù)不同的字段進(jìn)行升序或者降序的展示格郁。

這個(gè)應(yīng)用程序看起來(lái)很簡(jiǎn)單展箱,你可以想出好幾種不同的方式來(lái)寫(xiě)旨枯。最容易想到的可能是,在你的 JavaScript 代碼里面存儲(chǔ)這樣的數(shù)據(jù):

var sortKey = "new" // 排序的字段混驰,新增(new)攀隔、取消(cancel)、凈關(guān)注(gain)栖榨、累積(cumulate)人數(shù)
var sortType = 1 // 升序還是逆序
var data = [{...}, {...}, {..}, ..] // 表格數(shù)據(jù)

用三個(gè)字段分別存儲(chǔ)當(dāng)前排序的字段昆汹、排序方向、還有表格數(shù)據(jù)婴栽;然后給表格頭部加點(diǎn)擊事件:當(dāng)用戶點(diǎn)擊特定的字段的時(shí)候满粗,根據(jù)上面幾個(gè)字段存儲(chǔ)的內(nèi)容來(lái)對(duì)內(nèi)容進(jìn)行排序,然后用 JS 或者 jQuery 操作 DOM愚争,更新頁(yè)面的排序狀態(tài)(表頭的那幾個(gè)箭頭表示當(dāng)前排序狀態(tài)映皆,也需要更新)和表格內(nèi)容。

這樣做會(huì)導(dǎo)致的后果就是轰枝,隨著應(yīng)用程序越來(lái)越復(fù)雜捅彻,需要在JS里面維護(hù)的字段也越來(lái)越多,需要監(jiān)聽(tīng)事件和在事件回調(diào)用更新頁(yè)面的DOM操作也越來(lái)越多狸膏,應(yīng)用程序會(huì)變得非常難維護(hù)沟饥。后來(lái)人們使用了 MVC、MVP 的架構(gòu)模式湾戳,希望能從代碼組織方式來(lái)降低維護(hù)這種復(fù)雜應(yīng)用程序的難度贤旷。但是 MVC 架構(gòu)沒(méi)辦法減少你所維護(hù)的狀態(tài),也沒(méi)有降低狀態(tài)更新你需要對(duì)頁(yè)面的更新操作(前端來(lái)說(shuō)就是DOM操作)砾脑,你需要操作的DOM還是需要操作幼驶,只是換了個(gè)地方。

既然狀態(tài)改變了要操作相應(yīng)的DOM元素韧衣,為什么不做一個(gè)東西可以讓視圖和狀態(tài)進(jìn)行綁定盅藻,狀態(tài)變更了視圖自動(dòng)變更,就不用手動(dòng)更新頁(yè)面了畅铭。這就是后來(lái)人們想出了 MVVM 模式氏淑,只要在模版中聲明視圖組件是和什么狀態(tài)進(jìn)行綁定的,雙向綁定引擎就會(huì)在狀態(tài)更新的時(shí)候自動(dòng)更新視圖(關(guān)于MV*模式的內(nèi)容硕噩,可以看這篇介紹)假残。

MVVM 可以很好的降低我們維護(hù)狀態(tài) -> 視圖的復(fù)雜程度(大大減少代碼中的視圖更新邏輯)。但是這不是唯一的辦法,還有一個(gè)非常直觀的方法辉懒,可以大大降低視圖更新的操作:一旦狀態(tài)發(fā)生了變化阳惹,就用模版引擎重新渲染整個(gè)視圖,然后用新的視圖更換掉舊的視圖眶俩。就像上面的表格莹汤,當(dāng)用戶點(diǎn)擊的時(shí)候,還是在JS里面更新?tīng)顟B(tài)颠印,但是頁(yè)面更新就不用手動(dòng)操作 DOM 了纲岭,直接把整個(gè)表格用模版引擎重新渲染一遍,然后設(shè)置一下innerHTML就完事了嗽仪。

聽(tīng)到這樣的做法荒勇,經(jīng)驗(yàn)豐富的你一定第一時(shí)間意識(shí)這樣的做法會(huì)導(dǎo)致很多的問(wèn)題。最大的問(wèn)題就是這樣做會(huì)很慢闻坚,因?yàn)榧词挂粋€(gè)小小的狀態(tài)變更都要重新構(gòu)造整棵 DOM,性價(jià)比太低兢孝;而且這樣做的話窿凤,inputtextarea的會(huì)失去原有的焦點(diǎn)。最后的結(jié)論會(huì)是:對(duì)于局部的小視圖的更新跨蟹,沒(méi)有問(wèn)題(Backbone就是這么干的)雳殊;但是對(duì)于大型視圖,如全局應(yīng)用狀態(tài)變更的時(shí)候窗轩,需要更新頁(yè)面較多局部視圖的時(shí)候夯秃,這樣的做法不可取。

但是這里要明白和記住這種做法痢艺,因?yàn)楹竺婺銜?huì)發(fā)現(xiàn)仓洼,其實(shí) Virtual DOM 就是這么做的,只是加了一些特別的步驟來(lái)避免了整棵 DOM 樹(shù)變更堤舒。

另外一點(diǎn)需要注意的就是色建,上面提供的幾種方法,其實(shí)都在解決同一個(gè)問(wèn)題:維護(hù)狀態(tài)舌缤,更新視圖箕戳。在一般的應(yīng)用當(dāng)中,如果能夠很好方案來(lái)應(yīng)對(duì)這個(gè)問(wèn)題国撵,那么就幾乎降低了大部分復(fù)雜性陵吸。

3 Virtual DOM算法

DOM是很慢的。如果我們把一個(gè)簡(jiǎn)單的div元素的屬性都打印出來(lái)介牙,你會(huì)看到:

而這僅僅是第一層壮虫。真正的 DOM 元素非常龐大,這是因?yàn)闃?biāo)準(zhǔn)就是這么設(shè)計(jì)的耻瑟。而且操作它們的時(shí)候你要小心翼翼旨指,輕微的觸碰可能就會(huì)導(dǎo)致頁(yè)面重排赏酥,這可是殺死性能的罪魁禍?zhǔn)住?/p>

相對(duì)于 DOM 對(duì)象,原生的 JavaScript 對(duì)象處理起來(lái)更快谆构,而且更簡(jiǎn)單裸扶。DOM 樹(shù)上的結(jié)構(gòu)、屬性信息我們都可以很容易地用 JavaScript 對(duì)象表示出來(lái):

var element = {
  tagName: 'ul', // 節(jié)點(diǎn)標(biāo)簽名
  props: { // DOM的屬性搬素,用一個(gè)對(duì)象存儲(chǔ)鍵值對(duì)
    id: 'list'
  },
  children: [ // 該節(jié)點(diǎn)的子節(jié)點(diǎn)
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

上面對(duì)應(yīng)的HTML寫(xiě)法是:

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

既然原來(lái) DOM 樹(shù)的信息都可以用 JavaScript 對(duì)象來(lái)表示呵晨,反過(guò)來(lái),你就可以根據(jù)這個(gè)用 JavaScript 對(duì)象表示的樹(shù)結(jié)構(gòu)來(lái)構(gòu)建一棵真正的DOM樹(shù)熬尺。

之前的章節(jié)所說(shuō)的摸屠,狀態(tài)變更->重新渲染整個(gè)視圖的方式可以稍微修改一下:用 JavaScript 對(duì)象表示 DOM 信息和結(jié)構(gòu),當(dāng)狀態(tài)變更的時(shí)候粱哼,重新渲染這個(gè) JavaScript 的對(duì)象結(jié)構(gòu)季二。當(dāng)然這樣做其實(shí)沒(méi)什么卵用,因?yàn)檎嬲捻?yè)面其實(shí)沒(méi)有改變揭措。

但是可以用新渲染的對(duì)象樹(shù)去和舊的樹(shù)進(jìn)行對(duì)比胯舷,記錄這兩棵樹(shù)差異。記錄下來(lái)的不同就是我們需要對(duì)頁(yè)面真正的 DOM 操作绊含,然后把它們應(yīng)用在真正的 DOM 樹(shù)上桑嘶,頁(yè)面就變更了。這樣就可以做到:視圖的結(jié)構(gòu)確實(shí)是整個(gè)全新渲染了躬充,但是最后操作DOM的時(shí)候確實(shí)只變更有不同的地方逃顶。

這就是所謂的 Virtual DOM 算法。包括幾個(gè)步驟:

  1. 用 JavaScript 對(duì)象結(jié)構(gòu)表示 DOM 樹(shù)的結(jié)構(gòu)充甚;然后用這個(gè)樹(shù)構(gòu)建一個(gè)真正的 DOM 樹(shù)以政,插到文檔當(dāng)中
  2. 當(dāng)狀態(tài)變更的時(shí)候,重新構(gòu)造一棵新的對(duì)象樹(shù)津坑。然后用新的樹(shù)和舊的樹(shù)進(jìn)行比較妙蔗,記錄兩棵樹(shù)差異
  3. 把2所記錄的差異應(yīng)用到步驟1所構(gòu)建的真正的DOM樹(shù)上,視圖就更新了

Virtual DOM 本質(zhì)上就是在 JS 和 DOM 之間做了一個(gè)緩存疆瑰∶挤矗可以類比 CPU 和硬盤(pán),既然硬盤(pán)這么慢穆役,我們就在它們之間加個(gè)緩存:既然 DOM 這么慢寸五,我們就在它們 JS 和 DOM 之間加個(gè)緩存。CPU(JS)只操作內(nèi)存(Virtual DOM)耿币,最后的時(shí)候再把變更寫(xiě)入硬盤(pán)(DOM)梳杏。

4 算法實(shí)現(xiàn)

4.1 步驟一:用JS對(duì)象模擬DOM樹(shù)

用 JavaScript 來(lái)表示一個(gè) DOM 節(jié)點(diǎn)是很簡(jiǎn)單的事情,你只需要記錄它的節(jié)點(diǎn)類型、屬性十性,還有子節(jié)點(diǎn):

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 結(jié)構(gòu)就可以簡(jiǎn)單的表示:

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'])
])

現(xiàn)在ul只是一個(gè) JavaScript 對(duì)象表示的 DOM 結(jié)構(gòu)叛溢,頁(yè)面上并沒(méi)有這個(gè)結(jié)構(gòu)。我們可以根據(jù)這個(gè)ul構(gòu)建真正的<ul>

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

  for (var propName in props) { // 設(shè)置節(jié)點(diǎn)的DOM屬性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子節(jié)點(diǎn)也是虛擬DOM劲适,遞歸構(gòu)建DOM節(jié)點(diǎn)
      : document.createTextNode(child) // 如果字符串楷掉,只構(gòu)建文本節(jié)點(diǎn)
    el.appendChild(childEl)
  })

  return el
}

render方法會(huì)根據(jù)tagName構(gòu)建一個(gè)真正的DOM節(jié)點(diǎn),然后設(shè)置這個(gè)節(jié)點(diǎn)的屬性霞势,最后遞歸地把自己的子節(jié)點(diǎn)也構(gòu)建起來(lái)烹植。所以只需要:

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

上面的ulRoot是真正的DOM節(jié)點(diǎn),把它塞入文檔中愕贡,這樣body里面就有了真正的<ul>的DOM結(jié)構(gòu):

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

完整代碼可見(jiàn)

element.js草雕。

4.2 步驟二:比較兩棵虛擬DOM樹(shù)的差異

正如你所預(yù)料的,比較兩棵DOM樹(shù)的差異是 Virtual DOM 算法最核心的部分固以,這也是所謂的 Virtual DOM 的 diff 算法墩虹。兩個(gè)樹(shù)的完全的 diff 算法是一個(gè)時(shí)間復(fù)雜度為 O(n^3) 的問(wèn)題。但是在前端當(dāng)中嘴纺,你很少會(huì)跨越層級(jí)地移動(dòng)DOM元素败晴。所以 Virtual DOM 只會(huì)對(duì)同一個(gè)層級(jí)的元素進(jìn)行對(duì)比:

上面的div只會(huì)和同一層級(jí)的div對(duì)比,第二層級(jí)的只會(huì)跟第二層級(jí)對(duì)比栽渴。這樣算法復(fù)雜度就可以達(dá)到 O(n)。

4.2.1 深度優(yōu)先遍歷稳懒,記錄差異

在實(shí)際的代碼中闲擦,會(huì)對(duì)新舊兩棵樹(shù)進(jìn)行一個(gè)深度優(yōu)先的遍歷,這樣每個(gè)節(jié)點(diǎn)都會(huì)有一個(gè)唯一的標(biāo)記:

在深度優(yōu)先遍歷的時(shí)候场梆,每遍歷到一個(gè)節(jié)點(diǎn)就把該節(jié)點(diǎn)和新的的樹(shù)進(jìn)行對(duì)比墅冷。如果有差異的話就記錄到一個(gè)對(duì)象里面。

// diff 函數(shù)或油,對(duì)比兩棵樹(shù)
function diff (oldTree, newTree) {
  var index = 0 // 當(dāng)前節(jié)點(diǎn)的標(biāo)志
  var patches = {} // 用來(lái)記錄每個(gè)節(jié)點(diǎn)差異的對(duì)象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對(duì)兩棵樹(shù)進(jìn)行深度優(yōu)先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
  // 對(duì)比oldNode和newNode的不同寞忿,記錄下來(lái)
  patches[index] = [...]

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

// 遍歷子節(jié)點(diǎn)
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) // 計(jì)算節(jié)點(diǎn)的標(biāo)識(shí)
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節(jié)點(diǎn)
    leftNode = child
  })
}

例如,上面的div和新的div有差異顶岸,當(dāng)前的標(biāo)記是0腔彰,那么:

patches[0] = [{difference}, {difference}, ...] // 用數(shù)組存儲(chǔ)新舊節(jié)點(diǎn)的不同

同理ppatches[1]ulpatches[3]辖佣,類推霹抛。

4.2.2 差異類型

上面說(shuō)的節(jié)點(diǎn)的差異指的是什么呢?對(duì) DOM 操作可能會(huì):

  1. 替換掉原來(lái)的節(jié)點(diǎn)卷谈,例如把上面的div換成了section
  2. 移動(dòng)杯拐、刪除、新增子節(jié)點(diǎn),例如上面div的子節(jié)點(diǎn)端逼,把pul順序互換
  3. 修改了節(jié)點(diǎn)的屬性
  4. 對(duì)于文本節(jié)點(diǎn)朗兵,文本內(nèi)容可能會(huì)改變。例如修改上面的文本節(jié)點(diǎn)2內(nèi)容為Virtual DOM 2顶滩。

所以我們定義了幾種差異類型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

對(duì)于節(jié)點(diǎn)替換余掖,很簡(jiǎn)單。判斷新舊節(jié)點(diǎn)的tagName和是不是一樣的诲祸,如果不一樣的說(shuō)明需要替換掉浊吏。如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"
  }
}]

如果是文本節(jié)點(diǎn)找田,如上面的文本節(jié)點(diǎn)2,就記錄下:

patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]

那如果把我div的子節(jié)點(diǎn)重新排序呢着憨?例如p, ul, div的順序換成了div, p, ul墩衙。這個(gè)該怎么對(duì)比?如果按照同層級(jí)進(jìn)行順序?qū)Ρ鹊脑捈锥叮鼈兌紩?huì)被替換掉漆改。如pdivtagName不同,p會(huì)被div所替代准谚。最終挫剑,三個(gè)節(jié)點(diǎn)都會(huì)被替換,這樣DOM開(kāi)銷就非常大柱衔。而實(shí)際上是不需要替換節(jié)點(diǎn)樊破,而只需要經(jīng)過(guò)節(jié)點(diǎn)移動(dòng)就可以達(dá)到,我們只需知道怎么進(jìn)行移動(dòng)唆铐。

這牽涉到兩個(gè)列表的對(duì)比算法哲戚,需要另外起一個(gè)小節(jié)來(lái)討論。

4.2.3 列表對(duì)比算法

假設(shè)現(xiàn)在可以英文字母唯一地標(biāo)識(shí)每一個(gè)子節(jié)點(diǎn):

舊的節(jié)點(diǎn)順序:

a b c d e f g h i

現(xiàn)在對(duì)節(jié)點(diǎn)進(jìn)行了刪除艾岂、插入顺少、移動(dòng)的操作。新增j節(jié)點(diǎn)王浴,刪除e節(jié)點(diǎn)脆炎,移動(dòng)h節(jié)點(diǎn):

新的節(jié)點(diǎn)順序:

a b c h d f g i j

現(xiàn)在知道了新舊的順序,求最小的插入叼耙、刪除操作(移動(dòng)可以看成是刪除和插入操作的結(jié)合)腕窥。這個(gè)問(wèn)題抽象出來(lái)其實(shí)是字符串的最小編輯距離問(wèn)題(Edition Distance),最常見(jiàn)的解決算法是

Levenshtein Distance筛婉,通過(guò)動(dòng)態(tài)規(guī)劃求解簇爆,時(shí)間復(fù)雜度為 O(M * N)癞松。但是我們并不需要真的達(dá)到最小的操作,我們只需要優(yōu)化一些比較常見(jiàn)的移動(dòng)情況入蛆,犧牲一定DOM操作响蓉,讓算法時(shí)間復(fù)雜度達(dá)到線性的(O(max(M, N))。具體算法細(xì)節(jié)比較多哨毁,這里不累述枫甲,有興趣可以參考代碼

我們能夠獲取到某個(gè)父節(jié)點(diǎn)的子節(jié)點(diǎn)的操作扼褪,就可以記錄下來(lái):

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

但是要注意的是想幻,因?yàn)?code>tagName是可重復(fù)的,不能用這個(gè)來(lái)進(jìn)行對(duì)比话浇。所以需要給子節(jié)點(diǎn)加上唯一標(biāo)識(shí)key脏毯,列表對(duì)比的時(shí)候,使用key進(jìn)行對(duì)比幔崖,這樣才能復(fù)用老的 DOM 樹(shù)上的節(jié)點(diǎn)食店。

這樣,我們就可以通過(guò)深度優(yōu)先遍歷兩棵樹(shù)赏寇,每層的節(jié)點(diǎn)進(jìn)行對(duì)比吉嫩,記錄下每個(gè)節(jié)點(diǎn)的差異了。完整 diff 算法代碼可見(jiàn)

diff.js嗅定。

4.3 步驟三:把差異應(yīng)用到真正的DOM樹(shù)上

因?yàn)椴襟E一所構(gòu)建的 JavaScript 對(duì)象樹(shù)和render出來(lái)真正的DOM樹(shù)的信息自娩、結(jié)構(gòu)是一樣的。所以我們可以對(duì)那棵DOM樹(shù)也進(jìn)行深度優(yōu)先的遍歷渠退,遍歷的時(shí)候從步驟二生成的patches對(duì)象中找出當(dāng)前遍歷的節(jié)點(diǎn)差異椒功,然后進(jìn)行 DOM 操作。

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

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 從patches拿出當(dāng)前節(jié)點(diǎn)的差異

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

  if (currentPatches) {
    applyPatches(node, currentPatches) // 對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行DOM操作
  }
}

applyPatches智什,根據(jù)不同類型的差異對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行 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)
    }
  })
}

完整代碼可見(jiàn)

patch.js

5 結(jié)語(yǔ)

Virtual DOM 算法主要是實(shí)現(xiàn)上面步驟的三個(gè)函數(shù):element丁屎,diff荠锭,patch。然后就可以實(shí)際的進(jìn)行使用:

// 1\. 構(gòu)建虛擬DOM
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2\. 通過(guò)虛擬DOM構(gòu)建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3\. 生成新的虛擬DOM
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4\. 比較兩棵虛擬DOM樹(shù)的不同
var patches = diff(tree, newTree)

// 5\. 在真正的DOM元素上應(yīng)用變更
patch(root, patches)

當(dāng)然這是非常粗糙的實(shí)踐晨川,實(shí)際中還需要處理事件監(jiān)聽(tīng)等证九;生成虛擬 DOM 的時(shí)候也可以加入 JSX 語(yǔ)法。這些事情都做了的話共虑,就可以構(gòu)造一個(gè)簡(jiǎn)單的ReactJS了愧怜。

本文所實(shí)現(xiàn)的完整代碼存放在

Github,僅供學(xué)習(xí)妈拌。

6 References

https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拥坛,一起剝皮案震驚了整個(gè)濱河市蓬蝶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猜惋,老刑警劉巖丸氛,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異著摔,居然都是意外死亡缓窜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)谍咆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)禾锤,“玉大人,你說(shuō)我怎么就攤上這事摹察《髦溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵港粱,是天一觀的道長(zhǎng)螃成。 經(jīng)常有香客問(wèn)我,道長(zhǎng)查坪,這世上最難降的妖魔是什么寸宏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮偿曙,結(jié)果婚禮上氮凝,老公的妹妹穿的比我還像新娘。我一直安慰自己望忆,他們只是感情好罩阵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著启摄,像睡著了一般稿壁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歉备,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天傅是,我揣著相機(jī)與錄音,去河邊找鬼蕾羊。 笑死喧笔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的龟再。 我是一名探鬼主播书闸,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼利凑!你這毒婦竟也來(lái)了浆劲?” 一聲冷哼從身側(cè)響起嫌术,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梳侨,沒(méi)想到半個(gè)月后蛉威,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡走哺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蚯嫌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丙躏。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡择示,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晒旅,到底是詐尸還是另有隱情栅盲,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布废恋,位于F島的核電站谈秫,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏鱼鼓。R本人自食惡果不足惜拟烫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望迄本。 院中可真熱鬧硕淑,春花似錦、人聲如沸嘉赎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)公条。三九已至拇囊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間靶橱,已是汗流浹背寂拆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抓韩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓鬓长,卻偏偏與公主長(zhǎng)得像谒拴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涉波,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 參考文章:深度剖析:如何實(shí)現(xiàn)一個(gè)Virtual DOM 算法 作者:戴嘉華React中一個(gè)沒(méi)人能解釋清楚的問(wèn)題——...
    waka閱讀 5,952評(píng)論 0 21
  • 1.為什么需要虛擬DOM DOM是很慢的英上,其元素非常龐大炭序,頁(yè)面的性能問(wèn)題由JS引起的,大部分都是由DOM操作引起的...
    Ecl_02b8閱讀 2,202評(píng)論 1 4
  • Virtual DOM是React中的一個(gè)很重要的概念苍日,在日常開(kāi)發(fā)中惭聂,前端工程師們需要將后臺(tái)的數(shù)據(jù)呈現(xiàn)到界面中,同...
    SherHoooo閱讀 976評(píng)論 3 5
  • Think 在 react 之前我只接觸過(guò) mvvm 結(jié)構(gòu)的框架相恃。 mvvm 采用這樣一種機(jī)制:只要在模版中聲明視...
    Beginning丶2015閱讀 1,151評(píng)論 1 9
  • 先理解別人 再被人理解 …… 嗯 好像沒(méi)怎么做到過(guò) 尤其是我的家人 每當(dāng)我父母一對(duì)我提結(jié)婚二字 我就立馬紅臉 把他...
    劉妮娜閱讀 69評(píng)論 0 0