網(wǎng)上關(guān)于 Virtual DOM(下稱VDOM) 和 直接操作 DOM 來更新頁面誰的性能更高有許多爭論。這里就 VDOM 的性能問題簡單談下自己的理解。
一、什么是 VDOM
VDOM 本質(zhì)上是一個(gè) Javascript 對象,用來描述 DOM 結(jié)構(gòu),如:
<html>
<head></head>
<body>
<ul class="list-ul">
<li class="list-one">List One</li>
</ul>
</body>
</html>
可以用如下對象表示:
const vdom = {
tagName: "html",
children: [
{ tagName: "head" },
{
tagName: "body",
children: [
{
tagName: "ul",
attributes: { "class": "list-ul" },
children: [
{
tagName: "li",
attributes: { "class": "list-one" },
textContent: "List One"
}
]
}
]
}
]
}
在實(shí)際的生產(chǎn)環(huán)境需要將這個(gè) JS 對象轉(zhuǎn)化成真實(shí)的 DOM 元素。
二、Js 操作 DOM 的開銷
1、瀏覽器渲染引擎工作流程大致分為如下步驟:
-
解析 HTML 創(chuàng)建 DOM 樹
當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)。 - 解析 CSS 創(chuàng)建 CSS 規(guī)則樹
- 合并 DOM 樹和 CSS規(guī)則,生成 render 樹
- Layout / Reflow:布局 render 樹,計(jì)算各個(gè)元素的尺寸和位置等
- Paint:繪制頁面內(nèi)容
2、JS 頻繁操 DOM 的開銷:
- 在上述渲染過程中,前3點(diǎn)可能要多次執(zhí)行,比如 Js 操作 DOM、更改 CSS 樣式時(shí),瀏覽器又要重新構(gòu)建 DOM、CSS 規(guī)則樹,重新 render,重新 Layout、Paint;
- Layout 在 Paint 之前,因此每次 Layout 重新布局(Reflow 回流)后都要重新 Paint / 渲染,這時(shí)又要去消耗 GPU 資源
- Paint 不一定會觸發(fā) Layout,比如顏色、背景的改變,只需要 Repaint / 重繪
- 圖片下載完也會重新出發(fā) Layout 和 Paint
- 雖然瀏覽器針對渲染流程有優(yōu)化,但是這個(gè)過程開銷還是巨大的。
三、為什么需要 VDOM
假如在實(shí)際生產(chǎn)環(huán)境中,有這么一個(gè)列表:
<ul>
<li><span>item 1</span></li>
<li><span>item 2</span></li>
<li><span>item 3</span></li>
...
<li><span>item 100</span></li>
</ul>
我們現(xiàn)在需要更新列表的,從后端拿到了數(shù)據(jù),但是新的數(shù)據(jù)只有第50行的數(shù)據(jù)有變化。
1、最簡單最節(jié)約心智的辦法是,我們不關(guān)心新數(shù)據(jù)與老數(shù)據(jù)之間的差異,直接 innerHTML 更新整個(gè) ul 標(biāo)簽里的內(nèi)容。但是這樣就造成了不必要的 DOM 操作開銷,畢竟只需要更新一個(gè)節(jié)點(diǎn),但是為了省事,99次操作是浪費(fèi)資源的無用功。
2、或者我們逐個(gè)對比數(shù)據(jù),發(fā)現(xiàn)是第50行有變化,直接將第50行的 span 用 innerText 更新內(nèi)容即可。
3、雖然手動更新第50行內(nèi)容達(dá)到了最小操作,但是每次從后端拿到新數(shù)據(jù),我們并不能都知道是哪些行數(shù)據(jù)有變化,這個(gè)時(shí)候就需要寫一個(gè)通用的方法來比較新舊數(shù)據(jù)的變化,并只去更新數(shù)據(jù)有變化的節(jié)點(diǎn)。
但是這樣還不夠,這個(gè)通用方法也只是滿足了這一個(gè)列表的數(shù)據(jù)對比和節(jié)點(diǎn)更新,如果我們的項(xiàng)目中還有幾十、幾百個(gè)其它的列表呢?
這個(gè)時(shí)候 VDOM 就派上用場了。我們把整個(gè)頁面抽象成 Js 對象(VDOM),每次的更新數(shù)據(jù),我們都先更新 VDOM,再通過比較 新舊 VDOM 的變化,找到具體要更新的節(jié)點(diǎn),再去操作具體的 DOM。
這個(gè)時(shí)候可能有人要問了,那在更新 VDOM 的時(shí)候不也做了很多無用功的操作嗎?
對,但是 VDOM 的操作都是純 Js 的計(jì)算,大家要明確的一點(diǎn)是 Js 計(jì)算(特別是在 V8 引擎的加持下)要比真實(shí) DOM 操作開銷小得多,最重要的是再也不用操心到底哪些數(shù)據(jù)有更新了,直接無腦用 VDOM 就是,開發(fā)效率提高了!
四、那到底誰的效率更高
用這個(gè)列表舉例:
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
...
<li>item 100</li>
</ul>
1、當(dāng)列表新數(shù)據(jù)更新了第50、60行時(shí):
- 手動對第50、60行的 span 節(jié)點(diǎn)進(jìn)行 innerText操作,這是性能最高的,我們計(jì)它的耗時(shí)為
。
- 直接 innerHTML 整個(gè) ul 列表,不考慮瀏覽器渲染優(yōu)化等情況,簡單的認(rèn)為會多出88次節(jié)點(diǎn)操作,計(jì)為
。
- 通過 VDOM 來更新 DOM :
抽象 DOM 結(jié)構(gòu)為 VDOM -> Diff 策略比較變化 -> 更新真實(shí) DOM
則耗時(shí)為 Js 計(jì)算 VDOM 變化()與最小節(jié)點(diǎn)操作(
)之和,計(jì)為:
+
。
前面也說了得益于現(xiàn)代瀏覽器的高效,Js 計(jì)算是非常快的,故 +
<<
。
綜合比較三種更新方式, <
+
<<
。
可以看出手動去進(jìn)行最小節(jié)點(diǎn)操作是性能最好的,但是其心智負(fù)擔(dān)也不小。
2、當(dāng) ul 中所有數(shù)據(jù)都變了,那就能直接無腦 innerHTML 進(jìn)行更新,因?yàn)榇藭r(shí)更新所有節(jié)點(diǎn)就是最小節(jié)點(diǎn)操作。
所以一個(gè)項(xiàng)目能確定基本上每一頁的內(nèi)容都不相同,幾乎要全部更新,那可以不用 VDOM,直接用 innerHTML 即可。
反之一個(gè)項(xiàng)目,有很多列表,有眾多增刪改查操作,那用 VDOM 是十分有意義的。
所以拋開場景談性能就如同拋開劑量談毒性,都是耍流氓。用 VDOM 更新真實(shí) DOM 其實(shí)是在節(jié)約開銷(運(yùn)行效率)和節(jié)約心智(省事、提高開發(fā)效率)之間找到一個(gè)比較好的平衡點(diǎn)。不然為什么人家 React 和 Vue 要用 VDOM 呢?成千上萬人驗(yàn)證過的東西,能流行一定有它的道理