Virtual DOM 認知誤區(qū)

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

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

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




