Virtual DOM是React中的一個(gè)很重要的概念,在日常開(kāi)發(fā)中,前端工程師們需要將后臺(tái)的數(shù)據(jù)呈現(xiàn)到界面中,同時(shí)要能對(duì)用戶的操作提供反饋,作用到UI上。這些都離不開(kāi)DOM操作(還不清楚DOM是什么的也就不用往后看了。。),但是我們知道,頻繁的DOM操作會(huì)造成極大的資源浪費(fèi),也通常是性能瓶頸的原因。于是React 引入了Virtual DOM。
為什么我們需要Vitaul DOM?
前言中已經(jīng)說(shuō)到,引入Virtual DOM的原因是,避免頻繁的DOM操作。那DOM操作的性能到底是消耗到哪去了呢?我所查資料的觀點(diǎn)普遍是以下兩個(gè)方面:
- js訪問(wèn)DOM
- DOM操作引起的瀏覽器的重繪重排操作
抱著實(shí)踐出真知的態(tài)度,我先寫(xiě)了以下兩個(gè)demo進(jìn)行測(cè)試:
第一次對(duì)比試驗(yàn):


第一張圖中,訪問(wèn)了1000次DOM,并對(duì)其屬性進(jìn)行了操作(不會(huì)引起重繪重排),第二張圖,訪問(wèn)了1次DOM,但進(jìn)行了1000次字符串操作。但結(jié)果是相差甚大,并且1000次如此簡(jiǎn)單的DOM操作就消耗了6.315ms,這顯然是不能接受的。接下來(lái)看看,這6.315ms消耗去了哪里。


從以上Chrome Timeline時(shí)間線分析來(lái)看,主要還是script執(zhí)行時(shí)間的差異比較大。但看起來(lái)并不如之前的差異那么明顯。
第二次對(duì)比試驗(yàn):
這次我將DOM節(jié)點(diǎn)屬性操作替換成了innerHTML的內(nèi)容操作,我們?cè)賮?lái)看看效果


很明顯,總時(shí)間一下爆炸,同時(shí)對(duì)比上面的兩張圖,可以看到1000次的DOM節(jié)點(diǎn)的innerHTML內(nèi)容操作比1000次的DOM節(jié)點(diǎn)屬性操作整整翻了兩個(gè)數(shù)量級(jí)。再來(lái)看看Timeline。


消耗最為巨大的這次變成了Loading,先看看loading代表的是什么

可以看出,這里應(yīng)該是Parse HTML消耗了大量時(shí)間,也就是解析HTML
我們知道瀏覽器展現(xiàn)出我們的網(wǎng)頁(yè)是需要經(jīng)過(guò)這樣一個(gè)過(guò)程的:
瀏覽器解析DOM生成DOM樹(shù) + 解析CSS生成樣式樹(shù) => 進(jìn)行l(wèi)ayout布局和paint繪制 => 展現(xiàn)到設(shè)備上
那么從上面那張圖我們可以看出來(lái),JS操作DOM的innerHTML消耗了巨大的時(shí)間在Parse HTMl上,這里應(yīng)該沒(méi)有涉及到大面積的重繪重排,因?yàn)闀r(shí)間餅圖中render和paint的部分并沒(méi)有太多。但其消耗還是很龐大的,一千次微小的DOM操作就共用時(shí)接近3s。
由此可見(jiàn),Js操作DOM的確慢,而且真正的性能瓶頸應(yīng)該是引起了瀏覽器的后續(xù)操作,也就是解析HTML以及重繪重排等,這些都是非常消耗性能的。因此我們?yōu)榱吮M量避免因操作改變DOM而想了一些方式來(lái)進(jìn)行改善,例如使用Canvas替代原有的DOM動(dòng)畫(huà),或者使用Css3動(dòng)畫(huà)。以及,將列表數(shù)據(jù)渲染進(jìn)頁(yè)面時(shí),以一個(gè)根節(jié)點(diǎn)保存一次性插入,而不是每行數(shù)據(jù)都插入渲染。
當(dāng)然,在以前我們使用jQuery的時(shí)候,在我們對(duì)DOM性能優(yōu)化相當(dāng)了解并且能注意到的時(shí)候,是可以寫(xiě)出性能很高的代碼的,但是并不是所有工程師都能做到那一點(diǎn),因此,Virtual DOM就應(yīng)運(yùn)而生了。
Virtual DOM 是什么?
我所理解的Virtual DOM是一個(gè)黑盒,我們不需要去關(guān)心它如何映射渲染到DOM上,可以隨心所欲的去操作而不擔(dān)心性能消耗。那下面就讓我們具體來(lái)看一看Virtual DOM是什么吧。

如果說(shuō)DOM是一棵枝繁葉茂的樹(shù),那Virtual DOM就是一棵被修剪了很多多余的東西,只保留了最基礎(chǔ)的一個(gè)樹(shù)形的Js的數(shù)據(jù)結(jié)構(gòu)。
操作DOM會(huì)引發(fā)瀏覽器后續(xù)的操作,但操作Js數(shù)據(jù)結(jié)構(gòu)卻不會(huì)。我們以下面的例子來(lái)看看:

現(xiàn)在需要將上圖左邊的DOM結(jié)構(gòu)替換成右邊的結(jié)構(gòu),這種情景在實(shí)戰(zhàn)項(xiàng)目中是經(jīng)常會(huì)遇到的。但是如果直接操作DOM的話,進(jìn)行移除的話可能就是四次刪除,五次插入,這種消耗是很大的。但是使用Virtual DOM,那就是比較兩個(gè)結(jié)構(gòu)的差異,發(fā)現(xiàn)僅僅改變了四次內(nèi)容,一次插入。這種消耗就小很多,無(wú)非加上一個(gè)比較的時(shí)間。
React 中的Virtual DOM
在React中,我們知道,當(dāng)一個(gè)父組件的state發(fā)生改變的時(shí)候,會(huì)引發(fā)所有子組件的render,即使這個(gè)子組件本身的state是沒(méi)有改變的。這點(diǎn)在我剛學(xué)React的時(shí)候非常不能理解,因?yàn)槲矣X(jué)得這樣頻繁無(wú)故的渲染會(huì)造成性能浪費(fèi)。但是看完了Virtual DOM之后,我理解了,原來(lái)render
函數(shù)渲染出來(lái)的僅僅是Virtual DOM,因?yàn)椴僮鱆s的數(shù)據(jù)結(jié)構(gòu)是很快很方便的,所以即使重新渲染一遍Virtual DOM樹(shù)也是非常快的。同時(shí),渲染出來(lái)的組件的新的Virtual DOM樹(shù)會(huì)跟舊的Virtual DOM樹(shù)進(jìn)行差異比較,如果有修改,變動(dòng),那就將新的Virtual DOM渲染成DOM樹(shù)。如果沒(méi)有,則不變動(dòng)

簡(jiǎn)單實(shí)現(xiàn) 低配版 Virtual DOM
從前面的部分我們可以看到,Virtual DOM需要做的事情有兩件:
- 從DOM樹(shù)中模擬出相應(yīng)的Virtual DOM,Js的操作都作用在Virtual DOM上
- 以Diff算法比較新舊兩顆DOM樹(shù),找出其中的差異
- 將差異進(jìn)行處理,以最小代價(jià)渲染成DOM
吶,我們分析一下,如果想自己實(shí)現(xiàn)一個(gè)低配版我們需要做哪些事情呢?往下看。
以Js對(duì)象模擬出Virtual DOM樹(shù)
一棵樹(shù)嘛,最重要的是哪個(gè)部分?自然是每個(gè)樹(shù)的節(jié)點(diǎn)。那我們首先建立一個(gè)節(jié)點(diǎn)類。
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
var el = function (tagName, props, children) {
return new Element(tagName, props, children)
}
DOM節(jié)點(diǎn)中最重要的特征——標(biāo)簽名、屬性、以及子節(jié)點(diǎn),都在這個(gè)節(jié)點(diǎn)類里面有相應(yīng)表示。以這樣一個(gè)節(jié)點(diǎn)類的實(shí)例去表示DOM的一個(gè)節(jié)點(diǎn)。
吶,現(xiàn)在如果我們想將一堆DOM結(jié)構(gòu)以上面的節(jié)點(diǎn)類轉(zhuǎn)成虛擬DOM該怎么做呢?

是的,朋友們!就是這么簡(jiǎn)單!我們實(shí)現(xiàn)了從DOM到Virtual DOM的一個(gè)過(guò)程。但是,問(wèn)題又來(lái)了,當(dāng)我們想從Virtual DOM到DOM又該怎么辦呢?接著看。
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render()
: document.createElement(childEl)
})
return el
}
我們往Element類上面增加了一個(gè)靜態(tài)方法,這個(gè)方法用于將我們之前建立的節(jié)點(diǎn)實(shí)例還原成真實(shí)的DOM元素。
我們可以看到,這個(gè)方法內(nèi)部新建了一個(gè)對(duì)應(yīng)標(biāo)簽的DOM元素,并且將屬性名還原,最后遞歸調(diào)用,得到內(nèi)部子元素。返回該新建的DOM元素。
然而,到現(xiàn)在我們也只是完成了DOM與Virtual DOM之間的相互轉(zhuǎn)化。Virtual DOM實(shí)現(xiàn)的最重要的部分,其實(shí)還沒(méi)有完成,那就是Diff算法。真正的Diff算法我也不懂,這里也只能簡(jiǎn)單描述一下Virtual DOM中的Diff算法的思路。
- 首先對(duì)Virtual DOM樹(shù)進(jìn)行一個(gè)深度遍歷,也就是以深度優(yōu)先的原則進(jìn)行,對(duì)每一個(gè)遍歷到的Virtual DOM給一個(gè)標(biāo)記,譬如,頂層的div標(biāo)記為0。

這樣每個(gè)節(jié)點(diǎn)上都有我們的標(biāo)記,一旦Virtual DOM重新生成,在比較新舊兩顆Virtual DOM樹(shù)的時(shí)候,就可以直接比較對(duì)應(yīng)節(jié)點(diǎn)上的變化,如果有變化,則把這個(gè)變化與編號(hào)一起推入一個(gè)差異數(shù)組。
// 用數(shù)組存儲(chǔ)新舊節(jié)點(diǎn)的不同
patches[0] = [{difference}, {difference}, ...]
這樣我們就得到了這次Virtual DOM樹(shù)的一個(gè)改變內(nèi)容。但是推入進(jìn)差異數(shù)組中的差異到底怎么表示呢?首先這樣一個(gè)差異對(duì)象,起碼有一個(gè)type
屬性是用來(lái)表示這次差異的內(nèi)容吧,不然我哪知道是進(jìn)行了節(jié)點(diǎn)替換還是節(jié)點(diǎn)順序重排呢,這里我簡(jiǎn)單的列了幾個(gè),當(dāng)然真正的實(shí)踐中肯定不止這幾種類型,其他可以自行列舉。
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
當(dāng)我們知道了Virtual DOM上的差異之后,剩下的最后一步自然就是,將這個(gè)差異作用到DOM上。
既然Virtual DOM可以進(jìn)行深度遍歷標(biāo)記,那DOM與Virtual DOM是一樣的樹(shù)狀結(jié)構(gòu),自然也是可以進(jìn)行深度的遍歷標(biāo)記,那么標(biāo)記相同,自然就可以將那個(gè)差異數(shù)組上的每個(gè)差異對(duì)象對(duì)應(yīng)上其所匹配的DOM元素。而每個(gè)DOM元素拿到了其對(duì)應(yīng)差異(以currentPatches表示),就會(huì)依據(jù)差異的不同,進(jìn)行不同的處理。這里同樣列舉幾個(gè):
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)
}
})
}
這段代碼也是很好理解的吧,根據(jù)當(dāng)前差異currentPatches的類型type來(lái)進(jìn)行不同的DOM操作,這樣就能將Virtual DOM中有差異的部分,作用到DOM中,而那些并沒(méi)有差異的部分,就不需要進(jìn)行額外的變動(dòng),自然就節(jié)省了很多資源。
最后,Virtual DOM理解清楚一點(diǎn)之后,理解React會(huì)更清楚一些
第三部分實(shí)現(xiàn)低配版Virtual DOM代碼和思路源自深度剖析:如何實(shí)現(xiàn)一個(gè) Virtual DOM 算法, 如有侵權(quán),立刪。