【W(wǎng)eb技術(shù)】847- Virtual DOM 認(rèn)知誤區(qū)

作者:莫得鹽?
鏈接:https://juejin.cn/post/6898526276529684493
在當(dāng)下最流行的兩個(gè)前端框架都存在 Virtual DOM 的前提下, 漸漸比較多的聽(tīng)到類似“使用 Virtual DOM 有什么優(yōu)勢(shì)?” 的面試題,但一直沒(méi)有太在意。直到今天在寫(xiě)一個(gè)文檔時(shí),突讓想到要把“為什么需要 Virtual DOM ?”也寫(xiě)進(jìn)去,待我流暢的寫(xiě)好答案,略一思索——漏洞百出!也不知道是接納了哪方的知識(shí),讓我一直有能輕松回答這個(gè)問(wèn)題的錯(cuò)覺(jué), 其實(shí)對(duì)于這個(gè)問(wèn)題我是缺乏思考的。
你或許還不清楚我想說(shuō)什么,但請(qǐng)耐下心來(lái),先來(lái)看看網(wǎng)絡(luò)上關(guān)于此問(wèn)題的一些見(jiàn)解:
虛擬DOM同樣也是操作DOM,為啥說(shuō)它快?-- Segmentfault[1]
虛擬DOM不會(huì)進(jìn)行排版與重繪操作 虛擬DOM進(jìn)行頻繁修改,然后一次性比較并修改真實(shí)DOM中需要改的部分(注意!),最后并在真實(shí)DOM中進(jìn)行排版與重繪,減少過(guò)多DOM節(jié)點(diǎn)排版與重繪損耗 真實(shí)DOM頻繁排版與重繪的效率是相當(dāng)?shù)偷?/section> 虛擬DOM有效降低大面積(真實(shí)DOM節(jié)點(diǎn))的重繪與排版,因?yàn)樽罱K與真實(shí)DOM比較差異,可以只渲染局部(同2) Virtual Dom 的優(yōu)勢(shì)在哪里?-- Github[2]
具備跨平臺(tái)的優(yōu)勢(shì),由于 Virtual DOM 是以 JavaScript 對(duì)象為基礎(chǔ)而不依賴真實(shí)平臺(tái)環(huán)境,所以使它具有了跨平臺(tái)的能力,比如說(shuō)瀏覽器平臺(tái)、Weex、Node 等。 操作 DOM 慢,js運(yùn)行效率高。我們可以將DOM對(duì)比操作放在JS層,提高效率。因?yàn)镈OM操作的執(zhí)行速度遠(yuǎn)不如Javascript的運(yùn)算速度快,因此,把大量的DOM操作搬運(yùn)到Javascript中,運(yùn)用patching算法來(lái)計(jì)算出真正需要更新的節(jié)點(diǎn),最大限度地減少DOM操作,從而顯著提高性能。 提升渲染性能 Virtual DOM的優(yōu)勢(shì)不在于單次的操作,而是在大量、頻繁的數(shù)據(jù)更新下,能夠?qū)σ晥D進(jìn)行合理、高效的更新。 Virtual Dom的優(yōu)勢(shì) -- 掘金[3]
不會(huì)立即進(jìn)行排版與重繪; VDOM頻繁修改,一次性比較并修改真實(shí)DOM中需要修改的部分,最后在真實(shí)DOM中進(jìn)行重排 重繪,減少過(guò)多DOM節(jié)點(diǎn)重排重繪的性能消耗; VDOM有效降低大面積真實(shí)DOM的重繪與重排,與真實(shí)DOM比較差異,進(jìn)行局部渲染;
上面是從 Google 搜索到的三個(gè)平臺(tái)中的分析摘選,總結(jié)下來(lái)大概四點(diǎn):
操作 DOM 太慢,操作 Virtual DOM 對(duì)象快 使用 Virtual DOM 可以避免頻繁操作 DOM ,能有效減少回流和重繪次數(shù)(如果有的話) 有 diff 算法,可以減少?zèng)]必要的 DOM 操作 跨平臺(tái)優(yōu)勢(shì),只要有 JS 引擎就能運(yùn)行在任何地方(Weex/SSR)
它們的理解正確嗎?
本文測(cè)試數(shù)據(jù)都基于 Chrome 86.0.4240.198
Virtual DOM 快?
有人認(rèn)為操作 Virtual DOM 速度很快?Virtual DOM 是一個(gè)用來(lái)描述 DOM(注意,并不一定一一對(duì)應(yīng))的 Javascript 對(duì)象,Javascript 操作 Javascript 對(duì)象自然是快的。
但 Virtual DOM 仍然需要調(diào)用 DOM API 去生成真實(shí)的 DOM ,而你其實(shí)是可以直接調(diào)用它們的,所有就有一個(gè)很有意思結(jié)論,正數(shù)再小也不可能比零還小——Virtual DOM 很快,但這并不是它的優(yōu)勢(shì),因你本可以選擇不使用 Virtual DOM 。除了速度不是優(yōu)勢(shì),Virtual DOM 還有個(gè)最大的問(wèn)題——額外的內(nèi)存占用,以 Vue 的 Virtual DOM 對(duì)象為例,100W 個(gè)空的 Virtual DOM(Vue) 會(huì)占用 110M 內(nèi)存。
內(nèi)存占用截圖:
測(cè)試代碼:
let?creatVNode?=?function(type)?{
??return?{
????__v_isVNode:?true,
????SKIP:?true,
????type,
????props:?null,
????key:?null,
????ref:?null,
????scopeId:?0,
????children:?null,
????component:?null,
????suspense:?null,
????ssContent:?null,
????ssFallback:?null,
????dirs:?null,
????transition:?null,
????el:?null,
????anchor:?null,
????target:?null,
????targetAnchor:?null,
????staticCount:?0,
????shapeFlag:?0,
????patchFlag:?0,
????dynamicProps:?null,
????dynamicChildren:?null,
????appContext:?null
??}
}
let?counts?=?1000000
let?list?=?[]
let?start?=?performance.now()
//?創(chuàng)建?VNode(Vue)
//?10000:?1120k
for?(let?i?=?0;?i???list.push(creatVNode('div'))
}
//?創(chuàng)建?DOM
//?10000:?320k
//?for?(let?i?=?0;?i?
//???list.push(document.createElement('div'))
//?}
console.log(performance.now()?-?start)
令人意外的是 100W 個(gè)空的 DOM 對(duì)象只占用 45M 內(nèi)存,不清楚在 DOM 屬性明顯更多的情況下 Chrome 是如何優(yōu)化的,或則是 Dev Tools 存在問(wèn)題,希望有人能替我解惑。
你看 Virtual DOM 不但執(zhí)行快沒(méi)有用,還增加了大量的內(nèi)存消耗,所以我們說(shuō)它快自然是有問(wèn)題的,因?yàn)闆](méi)有 Virtual DOM 時(shí)更快。
Virtual DOM 減少回流和重繪?
也有人認(rèn)為 Virtual DOM 能減少頁(yè)面的 relayout 和 repaint ?通常有兩個(gè)原因來(lái)支撐這個(gè)觀點(diǎn):
DOM 操作會(huì)先改變 Virtual DOM ,所以一些無(wú)效該變(比如把文本 A 修改為 B ,然后再修改為 A)就不會(huì)調(diào)用 DOM API ,也就不會(huì)導(dǎo)致瀏覽做無(wú)效的回流和重繪。 DOM 操作會(huì)先改變 Virtual DOM ,最終由 Virtual DOM 調(diào)用 patch方法批量操作 DOM ,批量操作就不會(huì)導(dǎo)致過(guò)程中出現(xiàn)無(wú)意義的回流和重繪。
無(wú)效回流與重繪
第一個(gè)觀點(diǎn)看著很有道理,但有個(gè)問(wèn)題很難解釋:瀏覽器的 UI 線程在什么時(shí)候去執(zhí)行回流和重繪?要知道現(xiàn)代瀏覽器在設(shè)計(jì)上為了避免高復(fù)雜度,Javascript 線程和 UI 線程是互斥的,即如果瀏覽器要在 Javascript 執(zhí)行期間觸發(fā) relayout/repaint 則必須先掛起 Javascript 線程,這是個(gè)連我都覺(jué)得蠢的設(shè)計(jì),顯然不會(huì)出現(xiàn)在各大瀏覽器身上。
事實(shí)上也確實(shí)如此,無(wú)論你在一次事件循環(huán)中調(diào)用多少次的 DOM API ,瀏覽器也只會(huì)觸發(fā)一次回流與重繪(如果需要),并且如果多次調(diào)用并沒(méi)有修改 DOM 狀態(tài),那么回流與重繪一次都不會(huì)發(fā)生。
Timeline 截圖(沒(méi)有回流和重繪發(fā)生):
測(cè)試代碼:
<body>
??<div?class="app">div>
??<script>?let?counts?=?1000
????let?$app?=?document.querySelector('.app')
????setTimeout(()?=>?{
??????for?(let?i?=?0;?i?????????$app.innerHTML?=?'aaaa'
????????$app.style?=?'margin-top:?100px'
????????$app.innerHTML?=?''
????????$app.style?=?''
??????}
????},?1000)?script>
body>
無(wú)意義的回流與重繪
第二個(gè)觀點(diǎn)是比較有意思的,雖然看了上面的分析,你應(yīng)該也知道它是錯(cuò)的,批量操作并不能減少回流與重繪,因?yàn)樗鼈儽旧砭椭粫?huì)觸發(fā)一次。但我還是要列出來(lái)證明一下,因?yàn)檫@是我們當(dāng)下眾多前端的一個(gè)固有思維,我在準(zhǔn)備寫(xiě)這篇文章前問(wèn)了一下眾神交流群的朋友們,他們幾乎都掉進(jìn)了這個(gè)認(rèn)知陷阱中,認(rèn)為批量操作會(huì)減少回流與重繪。
批量操作并不能減少回流與重繪,原因也和上文一致,Javascript 是單線程且與 UI 線程互斥,所以直接放測(cè)試數(shù)據(jù):
Javascript 執(zhí)行耗時(shí)(數(shù)據(jù)取3次平均值):

Layout 耗時(shí)(數(shù)據(jù)取3次平均值):

測(cè)試代碼:
<body>
??<div?class="app">div>
??<script>?let?counts?=?1000
????let?$app?=?document.querySelector('.app')
????let?start?=?performance.now()
????//?單獨(dú)操作
????//?for?(let?i?=?0;?i?
????//???let?node?=?document.createTextNode(`${i},?`)
????//???$app.append(node)
????//?}
????//?批量操作
????let?$tempContainer?=?document.createElement('div')
????for?(let?i?=?0;?i???????let?node?=?document.createTextNode('node,')
??????$tempContainer.append(node)
????}
????$app.append($tempContainer)
????console.log(performance.now()?-?start)?script>
body>
可以看到的是,批量處理和單次處理再 Layout 期間耗時(shí)是幾乎一致的,雖然在 script 執(zhí)行階段還是存在一定的性能優(yōu)勢(shì)(大概 30%),但大抵上只要你用好 DOM 操作,批量或不批量帶來(lái)的性能影響是很小的( 10W 次調(diào)用多損耗 27ms )。
題外話:這里提出一個(gè)問(wèn)題,為什么在 script 執(zhí)行階段還是存在一定的性能差距?答案會(huì)在晚些時(shí)候公布(等我看完這部分邏輯)
Virtual DOM 有 diff 算法?
嚴(yán)格來(lái)說(shuō) diff 算法和 Virtual DOM 是兩個(gè)獨(dú)立的東西,二者互相之間也沒(méi)有充分必要的關(guān)聯(lián),比如 svelte[4] 沒(méi)有 Virtual DOM 也有其自己的 diff 算法。
但由于前端框架存在 Virtual DOM 就總有 diff 算法,并且使用了 Virtual DOM 對(duì) diff 算法也有兩個(gè)助力:
得益于 Virtual DOM 的抽象能力,diff 算法更容易被實(shí)現(xiàn)和理解 得益于 Virtual DOM Tree 總是在內(nèi)存中, diff 算法功能可以更強(qiáng)大(比如組件移動(dòng),沒(méi)有完整的 Tree 結(jié)構(gòu)是不可能實(shí)現(xiàn)的)
diff 算法能減少 DOM API 調(diào)用,顯然是存在設(shè)計(jì)和性能優(yōu)勢(shì)的,而由于 Virtual DOM 的存在,diff 算法可以更方便且更強(qiáng)大,所以我認(rèn)同這是 Virtual DOM 的優(yōu)勢(shì),但不能用“Virtual DOM 有 diff 算法”這樣的表述。
Virtual DOM 有跨平臺(tái)優(yōu)勢(shì)?
上文提到的 svelte 沒(méi)有 Virtual DOM ,但一樣可以實(shí)現(xiàn)服務(wù)端渲染,這說(shuō)明跨平臺(tái)并不依賴于 Virtual DOM 。
其實(shí)只要 Javascript 框架有實(shí)現(xiàn)平臺(tái) API 分發(fā)機(jī)制,就能在不同平臺(tái)執(zhí)行不同的渲染方法,即擁有跨平臺(tái)能力。這個(gè)能力的根本,是 Javascript 代碼能低代價(jià)地在各個(gè)平臺(tái)運(yùn)行(得利于瀏覽器在各個(gè)平臺(tái)的普及和 NodeJS),也就是常說(shuō)的 Javascript 的優(yōu)勢(shì)之一是跨平臺(tái)。所以把跨平臺(tái)當(dāng)做 Virtual DOM 的優(yōu)勢(shì),其實(shí)是不正確的,但我們或許應(yīng)該去思考下他們?yōu)槭裁磿?huì)這么認(rèn)為。
我的想法,可能是這兩個(gè)原因:
Virtual DOM 的優(yōu)勢(shì),可以在不接觸真實(shí) DOM 的情況下操作 DOM,并且性能更好
在 Virutal DOM 上的改動(dòng),最終還是會(huì)調(diào)用平臺(tái) API 去操作真實(shí)的 DOM ,所以沒(méi)有 Virtual DOM 只是相當(dāng)于少了一個(gè)中間抽象層,并不影響跨平臺(tái)能力有無(wú)。但還是需要明白,就目前的分析來(lái)看,這個(gè)抽象層對(duì)跨平臺(tái)能力還是提供了相當(dāng)大的方便(或者說(shuō)助力)的。
Virtual DOM 在 Vue 中很重要,Vue 本身就是一個(gè)圍繞 Virtual DOM 創(chuàng)建起來(lái)的框架,脫離了 Virtual DOM 其設(shè)計(jì)思想必然會(huì)和當(dāng)下迥乎不同
總結(jié)
本文從互聯(lián)網(wǎng)上摘選了部分對(duì)開(kāi)發(fā)者對(duì) Virtual DOM 優(yōu)點(diǎn)的認(rèn)知,也從現(xiàn)實(shí)生活中了解到一些誤解,總結(jié)為 Virtual DOM 的四個(gè)“優(yōu)勢(shì)”,并分別對(duì)這四個(gè)“優(yōu)勢(shì)”進(jìn)行了單獨(dú)分析或舉證。
最終我們識(shí)別了幾個(gè)關(guān)于 Virtual DOM 優(yōu)勢(shì)誤區(qū):
操作 DOM 太慢,操作 Virtual DOM 對(duì)象快 ?
Virtual DOM 很快,但這并不是它的優(yōu)勢(shì),因你本可以選擇不使用 Virtual DOM 。
使用 Virtual DOM 可以避免頻繁操作 DOM ,能有效減少回流和重繪次數(shù) ?
無(wú)論你在一次事件循環(huán)中調(diào)用多少次的 DOM API ,瀏覽器也只會(huì)觸發(fā)一次回流與重繪(如果需要),并且如果多次調(diào)用并沒(méi)有修改 DOM 狀態(tài),那么回流與重繪一次都不會(huì)發(fā)生。批量操作也不能減少回流與重繪。
Virtual DOM 有跨平臺(tái)優(yōu)勢(shì) ?
跨平臺(tái)是 Javascript 的優(yōu)勢(shì),與 Virtual DOM 無(wú)關(guān)。
我們也提到了 Virtual DOM 真正的優(yōu)點(diǎn)是其抽象能力和常駐內(nèi)存的特性,讓框架能更容易實(shí)現(xiàn)更強(qiáng)大的 diff 算法,缺點(diǎn)是增加了框架復(fù)雜度,也占用了更多的內(nèi)存。
參考資料
虛擬DOM同樣也是操作DOM,為啥說(shuō)它快?-- Segmentfault:https://segmentfault.com/q/1010000010303981
[2]Virtual Dom 的優(yōu)勢(shì)在哪里?-- Github:https://github.com/RomanHc/blog/issues/20
[3]Virtual Dom的優(yōu)勢(shì) -- 掘金:https://juejin.cn/post/6844904179715014669
[4]svelte:https://github.com/sveltejs/svelte

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 100+ 篇原創(chuàng)文章




