什么是虛擬DOM?
我們現(xiàn)在使用的三大主流框架Vue.js归粉、Angular和React都是聲明式操作DOM。我們通過描述狀態(tài)和DOM之間的映射關(guān)系是怎樣的,就可以將狀態(tài)渲染成試圖腻扇。關(guān)于狀態(tài)到視圖的轉(zhuǎn)化過程,框架會(huì)幫我們做砾嫉,不需要我們自己去操作DOM幼苛。
狀態(tài)可以是JavaScript中的任意類型。Object焕刮、Array舶沿、String、Number配并、Boolean等都可以作為狀態(tài)括荡,這些狀態(tài)可能最終會(huì)以段落、表單溉旋、鏈接或按鈕等元素呈現(xiàn)在用戶界面上畸冲。
本質(zhì)上,我們將狀態(tài)作為輸入,并生成DOM輸出在頁面上顯示出來邑闲,這個(gè)過程叫做渲染
然而通常在程序運(yùn)行時(shí)岩喷,狀態(tài)會(huì)不斷發(fā)生改變(狀態(tài)改變的原因有很多,可能是用戶點(diǎn)擊了某個(gè)按鈕监憎,可能是某個(gè)ajax請(qǐng)求纱意,這些行為都是異步的)每當(dāng)狀態(tài)發(fā)生變化時(shí),都需要重新渲染鲸阔。如何確定狀態(tài)中發(fā)生了什么變化以及需要在哪里更新DOM偷霉?
在這種情況下,最簡(jiǎn)單粗暴的方式是褐筛,不需要關(guān)心狀態(tài)發(fā)生了什么變化类少,不需要關(guān)心哪里更新DOM,我們只要把所有DOM刪除了渔扎,然后使用狀態(tài)重新生成一份DOM硫狞,并將其輸出到界面上。
但是訪問DOM是非常昂貴的晃痴,按照上面的方式残吩,會(huì)造成相當(dāng)多的性能浪費(fèi)。狀態(tài)變化通常只是有限的幾個(gè)節(jié)點(diǎn)需要重新渲染倘核,所有我們不僅需要找出哪里需要更新泣侮,還需要盡可能少的訪問DOM。
如上圖所示紧唱,當(dāng)某個(gè)狀態(tài)發(fā)生變化時(shí)活尊,只更新與這個(gè)狀態(tài)相關(guān)聯(lián)的DOM節(jié)點(diǎn)。
這個(gè)問題有很多種解決方案,目前,各大主流框架都有自己一套解決方案懈词,在Angular中就是臟檢查的流程祝高,React中使用虛擬DOM,vuejs1.0通過細(xì)粒度的綁定。因此,虛擬DOM本質(zhì)上只是眾多解決方案中的一種,可以用但并不一定必須用翎苫。
虛擬DOM的解決方式是通過狀態(tài)生成一個(gè)虛擬節(jié)點(diǎn)樹,然后使用虛擬節(jié)點(diǎn)樹進(jìn)行渲染榨了。在渲染之前煎谍,會(huì)使用新生成的虛擬節(jié)點(diǎn)數(shù)和上一次生成的虛擬節(jié)點(diǎn)樹進(jìn)行對(duì)比,只渲染不同的部分龙屉。
虛擬節(jié)點(diǎn)數(shù)其實(shí)是由組件樹建立起來的整個(gè)虛擬節(jié)點(diǎn)(Virtual Node呐粘,也簡(jiǎn)寫為Vnode)樹满俗。
為什么要引入虛擬DOM
事實(shí)上,Angular和React的變化偵測(cè)有一個(gè)共同點(diǎn)作岖,那就是他們都不知道哪些狀態(tài)變了唆垃。因此,就需要進(jìn)行比較暴力的對(duì)比痘儡,React是通過虛擬DOM的比對(duì)辕万,Angular是使用臟檢查的流程。
Vue.js的變化偵測(cè)不一樣沉删,它在一定程度上知道具體哪些狀態(tài)發(fā)生了變化渐尿,這樣就可以通過更細(xì)粒度的綁定來更新視圖。也就是說矾瑰,在Vue.js中砖茸,當(dāng)狀態(tài)發(fā)生變化時(shí),它在一定程度上知道哪些節(jié)點(diǎn)使用了這個(gè)狀態(tài)殴穴,從而對(duì)這些節(jié)點(diǎn)進(jìn)行更新操作凉夯,不需要對(duì)比。事實(shí)上采幌,在vue.js 1.0中就是這樣實(shí)現(xiàn)的劲够。
但是這樣做也有一定的代價(jià),因?yàn)榱6忍?xì)植榕,每一個(gè)綁定都會(huì)有一個(gè)對(duì)應(yīng)得watcher來觀察狀態(tài)的變化再沧,這樣就會(huì)有一定的內(nèi)存開銷和追蹤依賴的開銷。當(dāng)狀態(tài)被越多的節(jié)點(diǎn)使用時(shí)尊残,開銷就越大。大型項(xiàng)目來說淤堵,這個(gè)開銷是非常大寝衫。
因此,Vue.js 2.0中選擇了中等粒度的解決方案拐邪,那就是引入了虛擬DOM慰毅。組件級(jí)別是一個(gè)watcher實(shí)例,就是說即便一個(gè)組件內(nèi)有10個(gè)節(jié)點(diǎn)使用了某個(gè)狀態(tài)扎阶,但其實(shí)也只有一個(gè)watcher在觀察這個(gè)狀態(tài)的變化汹胃。所以這個(gè)狀態(tài)發(fā)生變化時(shí),只能通知到組件东臀,然后組件內(nèi)部通過虛擬DOM去進(jìn)行比對(duì)和渲染着饥。
Vue.js 中的虛擬DOM
在vue.js中,我們使用模板來描述狀態(tài)和DOM之間的映射關(guān)系惰赋。Vue.js通過編譯將模板轉(zhuǎn)化為渲染函數(shù)render宰掉,執(zhí)行渲染函數(shù)就可以得到一個(gè)虛擬節(jié)點(diǎn)樹呵哨,使用這個(gè)虛擬節(jié)點(diǎn)樹就可以渲染頁面。
虛擬DOM的終極目標(biāo)是將虛擬節(jié)點(diǎn)(vnode)渲染到視圖上轨奄。但是如果直接使用虛擬節(jié)點(diǎn)覆蓋舊節(jié)點(diǎn)的話孟害,會(huì)造成很多不必要的DOM操作。
例如一個(gè)ul標(biāo)簽下有很多l(xiāng)i標(biāo)簽挪拟,其中只有一個(gè)li變化挨务,這種情況下如果直接用新的ul替換舊的ul,其實(shí)除了那個(gè)發(fā)生了變化的li節(jié)點(diǎn)之外玉组,其他節(jié)點(diǎn)都不需要重新渲染耘子。
由于DOM操作比較慢,所以這些DOM操作在性能上會(huì)有一定的浪費(fèi)球切。避免這些不必要的DOM操作會(huì)提升很大的性能谷誓。
為了避免不必要的DOM操作,虛擬DOM在虛擬節(jié)點(diǎn)映射到視圖的過程中吨凑,將虛擬節(jié)點(diǎn)和上一次渲染視圖所使用的的舊虛擬節(jié)點(diǎn)(oldVnode)進(jìn)行對(duì)比捍歪。找出真正需要更新的節(jié)點(diǎn)來進(jìn)行DOM操作,可以避免不必要改動(dòng)的DOM鸵钝。
圖中給出了虛擬DOM的整體運(yùn)行流程糙臼,先將vnode和oldVnode做對(duì)比,然后在更新視圖
可以看出虛擬DOM在Vue.js中所做的事情并沒有那么復(fù)雜恩商,他主要做了兩件事
- 提供與真實(shí)DOM節(jié)點(diǎn)所對(duì)應(yīng)得虛擬節(jié)點(diǎn)vnode
- 將虛擬節(jié)點(diǎn)vnode和舊虛擬節(jié)點(diǎn)oldvnode進(jìn)行對(duì)比变逃,然后更新視圖。
vnode是JavaScript中一個(gè)很普通的對(duì)象怠堪,這個(gè)對(duì)象的屬性上保存了生成DOM節(jié)點(diǎn)所需要的一些數(shù)據(jù)揽乱。
對(duì)比兩個(gè)虛擬節(jié)點(diǎn)是虛擬DOM中最核心的算法(即patch),他可以判斷出哪些節(jié)點(diǎn)發(fā)生了變化粟矿,從而只對(duì)發(fā)生了變化的節(jié)點(diǎn)進(jìn)行操作凰棉。
總結(jié)
虛擬DOM是講狀態(tài)映射成試圖的眾多解決方案之一,它的運(yùn)作原理是使用狀態(tài)生成虛擬節(jié)點(diǎn)陌粹,然后使用虛擬節(jié)點(diǎn)渲染成視圖撒犀。
之所以需要先使用狀態(tài)生成虛擬節(jié)點(diǎn),是因?yàn)槿绻苯佑脿顟B(tài)生成真實(shí)的DOM掏秩,會(huì)有一定程度上的性能浪費(fèi)或舞。而先創(chuàng)建虛擬節(jié)點(diǎn)再渲染視圖,就可以將虛擬節(jié)點(diǎn)緩存蒙幻,然后使用新創(chuàng)建的虛擬節(jié)點(diǎn)和上一次緩存的虛擬節(jié)點(diǎn)進(jìn)行對(duì)比映凳,然后根據(jù)對(duì)比結(jié)果更新需要更新的DOM節(jié)點(diǎn),避免不必要的DOM操作杆煞。
由于Vue.js的變化偵測(cè)粒度更細(xì)魏宽,所以擋狀態(tài)發(fā)生變化時(shí)腐泻,vue.js知道的信息更多,一定程度上知道哪些位置使用了窗臺(tái)队询。因此派桩,vue.js可以通過細(xì)粒度的綁定來更新視圖,vue.js 1.0 就是這樣實(shí)現(xiàn)的蚌斩。
但是這么做也有一定的代價(jià)铆惑。因?yàn)榱6忍?xì),就會(huì)有很多的watcher同時(shí)觀察這些狀態(tài)送膳,會(huì)有一定的內(nèi)存開銷和依賴追蹤依賴的開銷员魏,所以vue.js 2.0 采取了中等粒度的解決方案。狀態(tài)偵測(cè)不再是某個(gè)具體節(jié)點(diǎn)叠聋,而是某個(gè)組件撕阎,組件內(nèi)部通過虛擬DOM來渲染視圖,這樣可以大大的縮減依賴數(shù)量和watcher數(shù)量碌补。
Vue.js中通過模板來描述狀態(tài)和視圖之間的映射關(guān)系虏束,所以會(huì)將模板編譯成渲染函數(shù)render,然后執(zhí)行渲染函數(shù)生成虛擬節(jié)點(diǎn)vnode,最后使用虛擬節(jié)點(diǎn)更新視圖厦章。
虛擬DOM在vue.js中所做的事是將虛擬節(jié)點(diǎn)vnode和舊虛擬節(jié)點(diǎn)oldVnode進(jìn)行對(duì)比镇匀,根據(jù)對(duì)比結(jié)果來進(jìn)行DOM操作來更新視圖。