虛擬DOM簡介

什么是虛擬DOM?

我們現(xiàn)在使用的三大主流框架Vue.js、Angular和React都是聲明式操作DOM。我們通過描述狀態(tài)和DOM之間的映射關(guān)系是怎樣的,就可以將狀態(tài)渲染成試圖。關(guān)于狀態(tài)到視圖的轉(zhuǎn)化過程,框架會幫我們做,不需要我們自己去操作DOM。
狀態(tài)可以是JavaScript中的任意類型。Object、Array、String、Number、Boolean等都可以作為狀態(tài),這些狀態(tài)可能最終會以段落、表單、鏈接或按鈕等元素呈現(xiàn)在用戶界面上。
本質(zhì)上,我們將狀態(tài)作為輸入,并生成DOM輸出在頁面上顯示出來,這個過程叫做渲染


渲染的過程.PNG

然而通常在程序運行時,狀態(tài)會不斷發(fā)生改變(狀態(tài)改變的原因有很多,可能是用戶點擊了某個按鈕,可能是某個ajax請求,這些行為都是異步的)每當(dāng)狀態(tài)發(fā)生變化時,都需要重新渲染。如何確定狀態(tài)中發(fā)生了什么變化以及需要在哪里更新DOM?
在這種情況下,最簡單粗暴的方式是,不需要關(guān)心狀態(tài)發(fā)生了什么變化,不需要關(guān)心哪里更新DOM,我們只要把所有DOM刪除了,然后使用狀態(tài)重新生成一份DOM,并將其輸出到界面上。
但是訪問DOM是非常昂貴的,按照上面的方式,會造成相當(dāng)多的性能浪費。狀態(tài)變化通常只是有限的幾個節(jié)點需要重新渲染,所有我們不僅需要找出哪里需要更新,還需要盡可能少的訪問DOM。


狀態(tài)發(fā)生變化.PNG

如上圖所示,當(dāng)某個狀態(tài)發(fā)生變化時,只更新與這個狀態(tài)相關(guān)聯(lián)的DOM節(jié)點。
這個問題有很多種解決方案,目前,各大主流框架都有自己一套解決方案,在Angular中就是臟檢查的流程,React中使用虛擬DOM,vuejs1.0通過細粒度的綁定。因此,虛擬DOM本質(zhì)上只是眾多解決方案中的一種,可以用但并不一定必須用。
虛擬DOM的解決方式是通過狀態(tài)生成一個虛擬節(jié)點樹,然后使用虛擬節(jié)點樹進行渲染。在渲染之前,會使用新生成的虛擬節(jié)點數(shù)和上一次生成的虛擬節(jié)點樹進行對比,只渲染不同的部分。
虛擬節(jié)點數(shù)其實是由組件樹建立起來的整個虛擬節(jié)點(Virtual Node,也簡寫為Vnode)樹。


虛擬節(jié)點樹.PNG

為什么要引入虛擬DOM

事實上,Angular和React的變化偵測有一個共同點,那就是他們都不知道哪些狀態(tài)變了。因此,就需要進行比較暴力的對比,React是通過虛擬DOM的比對,Angular是使用臟檢查的流程。
Vue.js的變化偵測不一樣,它在一定程度上知道具體哪些狀態(tài)發(fā)生了變化,這樣就可以通過更細粒度的綁定來更新視圖。也就是說,在Vue.js中,當(dāng)狀態(tài)發(fā)生變化時,它在一定程度上知道哪些節(jié)點使用了這個狀態(tài),從而對這些節(jié)點進行更新操作,不需要對比。事實上,在vue.js 1.0中就是這樣實現(xiàn)的。
但是這樣做也有一定的代價,因為粒度太細,每一個綁定都會有一個對應(yīng)得watcher來觀察狀態(tài)的變化,這樣就會有一定的內(nèi)存開銷和追蹤依賴的開銷。當(dāng)狀態(tài)被越多的節(jié)點使用時,開銷就越大。大型項目來說,這個開銷是非常大。
因此,Vue.js 2.0中選擇了中等粒度的解決方案,那就是引入了虛擬DOM。組件級別是一個watcher實例,就是說即便一個組件內(nèi)有10個節(jié)點使用了某個狀態(tài),但其實也只有一個watcher在觀察這個狀態(tài)的變化。所以這個狀態(tài)發(fā)生變化時,只能通知到組件,然后組件內(nèi)部通過虛擬DOM去進行比對和渲染。

Vue.js 中的虛擬DOM

在vue.js中,我們使用模板來描述狀態(tài)和DOM之間的映射關(guān)系。Vue.js通過編譯將模板轉(zhuǎn)化為渲染函數(shù)render,執(zhí)行渲染函數(shù)就可以得到一個虛擬節(jié)點樹,使用這個虛擬節(jié)點樹就可以渲染頁面。


模板轉(zhuǎn)化為視圖.PNG

虛擬DOM的終極目標(biāo)是將虛擬節(jié)點(vnode)渲染到視圖上。但是如果直接使用虛擬節(jié)點覆蓋舊節(jié)點的話,會造成很多不必要的DOM操作。
例如一個ul標(biāo)簽下有很多l(xiāng)i標(biāo)簽,其中只有一個li變化,這種情況下如果直接用新的ul替換舊的ul,其實除了那個發(fā)生了變化的li節(jié)點之外,其他節(jié)點都不需要重新渲染。
由于DOM操作比較慢,所以這些DOM操作在性能上會有一定的浪費。避免這些不必要的DOM操作會提升很大的性能。
為了避免不必要的DOM操作,虛擬DOM在虛擬節(jié)點映射到視圖的過程中,將虛擬節(jié)點和上一次渲染視圖所使用的的舊虛擬節(jié)點(oldVnode)進行對比。找出真正需要更新的節(jié)點來進行DOM操作,可以避免不必要改動的DOM。
圖中給出了虛擬DOM的整體運行流程,先將vnode和oldVnode做對比,然后在更新視圖


虛擬DOM執(zhí)行過程.PNG

可以看出虛擬DOM在Vue.js中所做的事情并沒有那么復(fù)雜,他主要做了兩件事
  • 提供與真實DOM節(jié)點所對應(yīng)得虛擬節(jié)點vnode
  • 將虛擬節(jié)點vnode和舊虛擬節(jié)點oldvnode進行對比,然后更新視圖。
    vnode是JavaScript中一個很普通的對象,這個對象的屬性上保存了生成DOM節(jié)點所需要的一些數(shù)據(jù)。
    對比兩個虛擬節(jié)點是虛擬DOM中最核心的算法(即patch),他可以判斷出哪些節(jié)點發(fā)生了變化,從而只對發(fā)生了變化的節(jié)點進行操作。

總結(jié)

虛擬DOM是講狀態(tài)映射成試圖的眾多解決方案之一,它的運作原理是使用狀態(tài)生成虛擬節(jié)點,然后使用虛擬節(jié)點渲染成視圖。
之所以需要先使用狀態(tài)生成虛擬節(jié)點,是因為如果直接用狀態(tài)生成真實的DOM,會有一定程度上的性能浪費。而先創(chuàng)建虛擬節(jié)點再渲染視圖,就可以將虛擬節(jié)點緩存,然后使用新創(chuàng)建的虛擬節(jié)點和上一次緩存的虛擬節(jié)點進行對比,然后根據(jù)對比結(jié)果更新需要更新的DOM節(jié)點,避免不必要的DOM操作。
由于Vue.js的變化偵測粒度更細,所以擋狀態(tài)發(fā)生變化時,vue.js知道的信息更多,一定程度上知道哪些位置使用了窗臺。因此,vue.js可以通過細粒度的綁定來更新視圖,vue.js 1.0 就是這樣實現(xiàn)的。
但是這么做也有一定的代價。因為粒度太細,就會有很多的watcher同時觀察這些狀態(tài),會有一定的內(nèi)存開銷和依賴追蹤依賴的開銷,所以vue.js 2.0 采取了中等粒度的解決方案。狀態(tài)偵測不再是某個具體節(jié)點,而是某個組件,組件內(nèi)部通過虛擬DOM來渲染視圖,這樣可以大大的縮減依賴數(shù)量和watcher數(shù)量。
Vue.js中通過模板來描述狀態(tài)和視圖之間的映射關(guān)系,所以會將模板編譯成渲染函數(shù)render,然后執(zhí)行渲染函數(shù)生成虛擬節(jié)點vnode,最后使用虛擬節(jié)點更新視圖。
虛擬DOM在vue.js中所做的事是將虛擬節(jié)點vnode和舊虛擬節(jié)點oldVnode進行對比,根據(jù)對比結(jié)果來進行DOM操作來更新視圖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容