前言
你有沒有留意到囱稽?優(yōu)秀的解決方案思想都是相通的:當(dāng)你研究 Flutter 渲染原理時(shí)會(huì)發(fā)現(xiàn) Flutter Rendering 層類似于 React 中的虛擬 DOM,當(dāng)你去看 Weex 工作原理時(shí)二跋,誒战惊,又發(fā)現(xiàn)了虛擬 DOM 的身影,更別提 VUE 響應(yīng)式視圖的核心也是虛擬 DOM 了同欠。
那這個(gè)虛擬 DOM 有什么用样傍?為什么這么多框架都應(yīng)用了它横缔?本質(zhì)上帶來了什么優(yōu)勢(shì)?本文將結(jié)合前端和移動(dòng)端來談?wù)劇?/p>
什么是 DOM衫哥?什么是虛擬 DOM茎刚?
DOM 就是文檔樹,與用戶界面控件樹對(duì)應(yīng)撤逢,在 web 開發(fā)中通常指 HTML 對(duì)應(yīng)的渲染樹膛锭,但廣義的 DOM 也可以指 Android 中的 XML 布局對(duì)應(yīng)的控件樹,而 DOM 操作就是指直接操作渲染樹(或控件樹)蚊荣。
虛擬 DOM 是一個(gè)用來表示真實(shí)的 DOM 結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)初狰。
思考
想當(dāng)年學(xué)前端的時(shí)候,還是 jQuery 的時(shí)代互例,想賦值奢入?改個(gè)樣式?取值媳叨?都是document.getElementById()
咔咔一頓操作腥光。這樣直接操作 DOM 會(huì)有什么問題?
直接操作 DOM 帶來的問題
1. model 和 view 耦合
最直觀的問題之一糊秆, 把用戶請(qǐng)求的表現(xiàn)邏輯和控制層要實(shí)現(xiàn)的業(yè)務(wù)邏輯兩者混合起來了武福,兩部分的依賴非常強(qiáng)。
2. 高頻操作引起性能損耗
寫個(gè)簡單Demo痘番,我們看下效果捉片。
為什么會(huì)有性能損耗?
原因可以歸結(jié)為 2 點(diǎn):
1. 跨界交流損耗
把 DOM 和 ECMAScript 各自想象成一個(gè)島嶼汞舱,它們之間用收費(fèi)橋梁連接伍纫。
????????????????????????????????????????????????????????????????????????????????——《高性能JavaScript》
DOM 屬于渲染引擎,而 JS 又是屬于 JS 引擎兵拢,在瀏覽器內(nèi)核中他們彼此獨(dú)立翻斟。單獨(dú)來看,兩者都是很快的说铃,但當(dāng)我們用 JS 去操作 DOM 時(shí)访惜,引擎之間進(jìn)行了“跨界交流”。這個(gè)“跨界交流”的實(shí)現(xiàn)并不簡單腻扇,它依賴了橋接接口作為“橋梁”债热,如下圖:
既然是收費(fèi)橋梁,過“橋”就要收費(fèi)幼苛。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值)窒篱,都要過一次“橋”。次數(shù)一多就會(huì)產(chǎn)生比較明顯的性能問題。
那移動(dòng)端混合開發(fā)的情況呢墙杯?
就拿 RectNative 舉例配并,RectNative 是一套 UI 基于原生控件(非Web UI)業(yè)務(wù)邏輯基于 JS 的跨平臺(tái)技術(shù)解決方案,JS 中所寫控件標(biāo)簽不是真實(shí)控件高镐,會(huì)在 Native 端解析為原生控件溉旋,如<Text>
標(biāo)簽對(duì)應(yīng) Android 中的 TextView 控件。
在布局過程中 RN 需要在 JS 和 Native 之間通信嫉髓,如果遇到滑動(dòng)和拖動(dòng)的情況观腊,劣勢(shì)就很明顯了,這和在瀏覽器中要 JS 頻繁操作 DOM 所帶來的問題是相同的算行,都會(huì)帶來比較可觀的性能開銷梧油。
2. DOM 修改引起重繪或重排
修改 DOM 屬性的代價(jià)更是昂貴儡陨,它會(huì)導(dǎo)致渲染引擎重新計(jì)算幾何變化(重排和重繪)。我們來看下渲染步驟:
在頁面生成時(shí)量淌,至少會(huì)進(jìn)行一次布局和渲染迄委,后面用戶操作時(shí),如果修改了 DOM 節(jié)點(diǎn)类少,會(huì)觸發(fā)渲染樹(Render Tree)的變化,從而進(jìn)行上圖的步驟2渔扎、3硫狞、4、5雁乡,因此如果在 js 中存在很多 DOM 操作囤官,就會(huì)不斷地觸發(fā)重繪或重排总放,影響頁面性能。
在移動(dòng)端泣侮,情況也好不到哪里去。
布局中的任何一個(gè) View 一旦發(fā)生屬性變化紧唱,都可能引起很大的連鎖反應(yīng)(如果所在的控件層級(jí)非常復(fù)雜的話)活尊。例如某個(gè) btn 的大小突然增加一倍,有可能會(huì)導(dǎo)致兄弟視圖的位置變化漏益,也有可能導(dǎo)致父視圖的大小發(fā)生改變蛹锰。當(dāng)大量的 layout() 操作被頻繁調(diào)用執(zhí)行時(shí),會(huì)引起整個(gè) View 頻繁地重渲绰疤,最終導(dǎo)致丟幀或 UI 卡頓铜犬。
解決辦法
針對(duì)以上的問題,我們一一提出解決方案:
1. 減少跨界過橋次數(shù),合并操作
ECMAScript 每次訪問 DOM癣猾,都要經(jīng)過這座橋敛劝,并交納“過橋費(fèi)”,訪問 DOM 的次數(shù)越多纷宇,費(fèi)用也就越高夸盟。因此,推薦的做法是盡量減少過橋的次數(shù)呐粘,努力呆在 ECMAScript 島上满俗。???????????????????????????????????????????????????——《高性能JavaScript》
我們來分析下,怎么減少“過橋的次數(shù)”作岖?過橋次數(shù)之所以頻繁唆垃,和頻繁的 DOM 操作有關(guān)。
比如我們給列表加數(shù)據(jù)痘儡,最差的方式就是這樣:
for (var i = 0; i < N; i++) {
var li = document.createElement("li");
li.innerHTML = arr[i];
ul.appendChild(li);
}
這里會(huì)操作 N 次 DOM 觸發(fā) N 次重繪辕万。重渲肯定是無法避免的,我們的目標(biāo)是最小化重繪和重排次數(shù)沉删。
那能不能不要立即去操作 DOM 呢渐尿?
將這 N 次更新的內(nèi)容保存到一個(gè) js 對(duì)象中,最終將這個(gè) js 對(duì)象一次性 attach 到 DOM 樹上矾瑰,通知瀏覽器去執(zhí)行繪制工作砖茸。這樣無論多么復(fù)雜的 DOM 操作,最終都只會(huì)觸發(fā)一次渲染全流程殴穴,避免了大量的無謂計(jì)算量凉夯,這樣不就可以了么!(欣喜若狂.jpg)
但優(yōu)化 DOM 操作方式很多采幌,不一定要依賴虛擬 DOM劲够,所以這不是我們需要虛擬 DOM 的根本原因,根本的原因還是響應(yīng)式需求休傍。
2. 響應(yīng)式
如果通過 JS 直接操作 DOM 的話征绎,勢(shì)必會(huì)造成視圖數(shù)據(jù)和模型數(shù)據(jù)的不匹配,我們能不能讓開發(fā)者只關(guān)心狀態(tài)(數(shù)據(jù))變化磨取,而無需關(guān)心控件操作呢人柿?當(dāng)然可以!
React 中提出一個(gè)重要思想:狀態(tài)改變則 UI 隨之自動(dòng)改變忙厌。
每次狀態(tài)有變動(dòng)就重構(gòu)用戶界面顷扩,重渲整個(gè) view。如果沒有虛擬 DOM慰毅,簡單粗暴的做法就是直接重置 innerHTML隘截,在大部分?jǐn)?shù)據(jù)都變了的情況下,重置 innerHTML 還算合理,但如果只有一行數(shù)據(jù)變了婶芭,顯然就有大量的浪費(fèi)东臀。
這是我們需要虛擬 DOM 的原因,用它來代替開發(fā)者的手工操作犀农,確保只對(duì)真正有變化的部分進(jìn)行實(shí)際的 DOM 操作(局部刷新)惰赋。
3. 總結(jié)
開發(fā)者對(duì)數(shù)據(jù)和狀態(tài)所做的任何改動(dòng),都會(huì)被自動(dòng)且高效的同步到虛擬 DOM(自動(dòng)同步呵哨,體現(xiàn)響應(yīng)式)赁濒,最后再批量同步到真實(shí) DOM 中,而不是每次改變都去操作一下 DOM(批量同步孟害,體現(xiàn)合并操作)
- 不需要直接操作控件拒炎,通過數(shù)據(jù)驅(qū)動(dòng)視圖
- 最大程度降低對(duì)最終視圖的修改,提高頁面渲染效率
怎么利用虛擬 DOM挨务?
1. React
當(dāng) React UI 渲染時(shí)击你,先渲染一個(gè)虛擬 DOM,這是一個(gè)輕量的純 js 的對(duì)象結(jié)構(gòu)谎柄,并沒有完全實(shí)現(xiàn) DOM丁侄,最主要的還是保留了節(jié)點(diǎn)之間的層次關(guān)系和一些基本屬性,因?yàn)?DOM 實(shí)在是太復(fù)雜朝巫,實(shí)際在做最后繪制時(shí)鸿摇,這些都是不需要關(guān)心的。所以虛擬 DOM 里每一個(gè)節(jié)點(diǎn)只有幾個(gè)簡單屬性劈猿,哪怕是直接把虛擬 DOM 刪了户辱,根據(jù)新傳進(jìn)來的數(shù)據(jù)重新創(chuàng)建一個(gè)新的虛擬 DOM 都非常快糙臼。
當(dāng)有變化時(shí),生成一個(gè)新的虛擬 DOM恩商。這個(gè)新的虛擬DOM反應(yīng)了數(shù)據(jù)模型的新狀態(tài)”涮樱現(xiàn)在我們有 2 個(gè)虛擬DOM:新的和老的。對(duì)比 DOM 樹差異得到一個(gè) Patch怠堪,把這個(gè) Patch 打到真實(shí)的 DOM 上去揽乱,這有點(diǎn)像版本控制打patch的思路。
那我們?cè)趺幢容^出兩顆 DOM 樹的差異呢粟矿? Diff 算法凰棉!
即給定任意兩棵樹,找到最少的轉(zhuǎn)換步驟陌粹。但是標(biāo)準(zhǔn)的 Diff 算法復(fù)雜度需要 O(n^3)撒犀,這顯然無法滿足性能要求。Facebook 工程師結(jié)合 Web 界面的特點(diǎn)做出了兩個(gè)簡單的假設(shè),使得 Diff 算法復(fù)雜度直接降低到 O(n)或舞。
- 兩個(gè)相同組件產(chǎn)生類似的 DOM 結(jié)構(gòu)荆姆,不同的組件產(chǎn)生不同的 DOM 結(jié)構(gòu);
- 對(duì)于同一層次的一組子節(jié)點(diǎn)映凳,它們可以通過唯一的 id 進(jìn)行區(qū)分胆筒。
算法上的優(yōu)化是 React 整個(gè)界面 Render 的基礎(chǔ),事實(shí)也證明這兩個(gè)假設(shè)是合理而精確的诈豌,保證了整體界面構(gòu)建的性能仆救。
由這一對(duì)不同類型的節(jié)點(diǎn)的處理邏輯我們很容易得到推論,那就是 React 的 DOM Diff 算法實(shí)際上只會(huì)對(duì)樹進(jìn)行逐層比較矫渔,如下圖:
React 只會(huì)對(duì)相同顏色方框內(nèi)的 DOM 節(jié)點(diǎn)進(jìn)行比較彤蔽,即同一個(gè)父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)。當(dāng)發(fā)現(xiàn)節(jié)點(diǎn)已經(jīng)不存在蚌斩,則該節(jié)點(diǎn)及其子節(jié)點(diǎn)會(huì)被完全刪除掉铆惑,不會(huì)用于進(jìn)一步的比較。這樣只需要對(duì)樹進(jìn)行一次遍歷送膳,便能完成整個(gè) DOM 樹的比較员魏。
實(shí)際實(shí)踐起來,Diff 算法并沒有這么簡單叠聋,感興趣的小伙伴可以在文末的推文去深入了解撕阎。
那跨平臺(tái)方案的情況呢?
2. RN
上文已經(jīng)提到 RN 是 React 在原生移動(dòng)應(yīng)用平臺(tái)的衍生產(chǎn)物碌补,那兩者主要的區(qū)別是什么呢虏束?主要的區(qū)別在于虛擬 DOM 映射的對(duì)象是什么。React 中虛擬 DOM 最終會(huì)映射為瀏覽器 DOM 樹厦章,而 RN 中虛擬 DOM 會(huì)通過 JavaScriptCore 映射為原生控件樹镇匀。
步驟如下:
- 布局消息傳遞:將虛擬 DOM 布局信息傳遞給原生;
- 原生根據(jù)布局信息袜啃,映射成對(duì)應(yīng)原生控件樹汗侵,渲染控件樹。
至此群发,RN 便實(shí)現(xiàn)了跨平臺(tái)晰韵。
3. weex
weex 一定程度上用 JS 實(shí)現(xiàn)了 vue 一統(tǒng)天下的效果。
可以看到熟妓,weex 會(huì)編譯構(gòu)建虛擬 DOM雪猪,并發(fā)送渲染指令給 RenderEngine 層,這樣起愈,同樣一份 JSON 數(shù)據(jù)只恨,在不同平臺(tái)的渲染引擎中能夠渲染成不同版本的 UI译仗,這是 Weex 可以實(shí)現(xiàn)動(dòng)態(tài)化的原因。
那三端的語法都不一樣坤次,Weex是怎么統(tǒng)一的古劲?重點(diǎn)在于 JS Framework!
weex 在 RN 的 JS V8 引擎基礎(chǔ)上缰猴,多了 JS Framework 承當(dāng)了重要的職責(zé)产艾,它主要負(fù)責(zé):管理 Weex 的生命周期;解析 JS Bundle滑绒,轉(zhuǎn)為 Virtual DOM闷堡,再通過所在平臺(tái)不同的 API 構(gòu)建頁面;進(jìn)行雙向的數(shù)據(jù)交互和響應(yīng)疑故。
這使得上層具備統(tǒng)一性杠览,在開發(fā)過程中,代碼模式纵势、編譯過程踱阿、模板組件、數(shù)據(jù)綁定钦铁、生命周期等上層語法是一致的软舌。得益于上層的統(tǒng)一,只需要在 JS Framework 層的最后判斷是由 Vue.js 生成真實(shí)的 DOM牛曹,還是通過 Native Api 渲染組件即可佛点。
4. Flutter
RN 和 React 原理相通,那 Flutter 呢黎比?Flutter Widget 的中心思想是用 Widget 構(gòu)建你的UI(非原生控件)超营。 那少了原生控件層和 js 層的通信損耗,不需要用虛擬 DOM 了吧阅虫?
非也演闭! Flutter Widget 從 React 中獲得了靈感,也是采用現(xiàn)代響應(yīng)式框架構(gòu)建颓帝。
先看看 Flutter 中三顆重要的樹:
-
Widget 樹:控件樹米碰,表示了我們?cè)?dart 代碼中所寫的控件的結(jié)構(gòu),但這只是描述信息躲履,渲染引擎是不認(rèn)識(shí)的。
Widget 被開發(fā)人員配置了多個(gè)屬性來定義它的展現(xiàn)形式聊闯,例如配置 Text 組件需要顯示的字符串工猜,配置輸入框組件需要顯示的內(nèi)容……Element 樹會(huì)記錄這些配置信息。
-
Element 數(shù):實(shí)際控件樹
在手機(jī)屏幕上顯示的控件并非我們?cè)诖a中所寫的 Widget菱蔬,F(xiàn)lutter 會(huì)根據(jù) Widget 樹信息生成控件對(duì)應(yīng)的 Element 樹篷帅,在 Flutter 中史侣,一個(gè) Widget 通過多次復(fù)用可以對(duì)應(yīng)多個(gè) Element 實(shí)例,Element 才是我們真正在屏幕上顯示的元素魏身。
Element 與 Widget 另一個(gè)區(qū)別在于惊橱,Widget 是不可變的,它的改變就意味著要重建箭昵,而其重建也非常頻繁税朴,如果我們將更多的任務(wù)交給它,將會(huì)對(duì)性能造成很大的損耗家制,因此我們把 Widget 樹當(dāng)作一個(gè)虛擬 DOM 樹正林,真正被渲染在屏幕上的其實(shí)是 ElememtTree,它持有其對(duì)應(yīng) Widget 的引用颤殴,如果對(duì)應(yīng)的 Widget 發(fā)生改變觅廓,它就會(huì)被標(biāo)記為 dirty Element,下一次更新視圖時(shí)根據(jù)這個(gè)狀態(tài)只更新被修改的內(nèi)容涵但,這樣就把可變狀態(tài)與 Widget 關(guān)聯(lián)起來杈绸,從而達(dá)到提升性能的效果。
RenderObject 樹:渲染樹矮瘟,做組件布局渲染工作瞳脓,包含渲染搭配、布局約束等信息芥永。
簡而言之篡殷,F(xiàn)lutter 引入虛擬 DOM 的目的是為了確定底層渲染樹從一個(gè)狀態(tài)轉(zhuǎn)換到下一個(gè)狀態(tài)所需的最小更改。
虛擬 DOM 對(duì)跨平臺(tái)技術(shù)的意義
那分析完各種跨平臺(tái)技術(shù)埋涧,你對(duì)虛擬 DOM 有了怎樣的認(rèn)識(shí)了呢板辽?
為什么使用虛擬 DOM?
是因?yàn)榭旒撸浚▽?shí)際上不一定快)
是因?yàn)榻怦睿?/p>
是因?yàn)轫憫?yīng)式劲弦?
對(duì)跨平臺(tái)技術(shù)來說,更重要的意義在于:
虛擬 DOM 是 DOM 在內(nèi)存中的一種輕量級(jí)表達(dá)方式醇坝,是一種統(tǒng)一約定邑跪!可以通過不同的渲染引擎生成不同平臺(tái)下的 UI!
虛擬 DOM 的可移植性非常好呼猪,這意味著可以渲染到 DOM 以外的任何端画畅,發(fā)揮你的想象力,可以做的事情很多宋距。
再次審視虛擬 DOM
虛擬 DOM 真正的價(jià)值從來都不是性能轴踱,而是不管數(shù)據(jù)怎么變化,都可以用最小的代價(jià)來更新 DOM谚赎,而且掩蓋了底層的 DOM 操作淫僻,讓你用更聲明式的方式來描述你的目的诱篷,從而讓你的代碼更容易維護(hù)。
虛擬 DOM 帶來了很多好思路雳灵,打開了通向有趣架構(gòu)的大門棕所,例如將視圖視為狀態(tài)函數(shù)。它讓我們編寫代碼悯辙,就像重新呈現(xiàn)整個(gè)場景一樣琳省。這不禁讓我感慨,沒有什么是加中間件不能解決的笑撞,如果有岛啸,那就再加多個(gè)中間件。
5 個(gè)詞語概括下意義:
可維護(hù)性茴肥、最小的代價(jià)坚踩、效率、函數(shù)式UI瓤狐、數(shù)據(jù)驅(qū)動(dòng)
進(jìn)一步思考
虛擬 DOM 的說明已經(jīng)結(jié)束了瞬铸,但是對(duì)于虛擬 DOM 的思考遠(yuǎn)沒有結(jié)束。
Rect 的方式有兩大缺點(diǎn):
每次數(shù)據(jù)更改础锐,哪怕改動(dòng)很小嗓节,都會(huì)生成完整的虛擬 DOM,如果 DOM 很復(fù)雜皆警,這個(gè)過程就會(huì)白白浪費(fèi)很多計(jì)算資源拦宣;
比較虛擬 DOM 差異的過程,既慢又容易出錯(cuò)信姓。因?yàn)?React 持有的新舊虛擬 DOM 相互獨(dú)立鸵隧,React 并不知道數(shù)據(jù)源發(fā)生了什么操作,只能根據(jù)兩個(gè)虛擬 DOM 來猜測需要執(zhí)行的操作意推。自動(dòng)的猜測算法既不準(zhǔn)又慢豆瘫,必須要前端開發(fā)者手動(dòng)提供 key 屬性和一些額外的方法實(shí)現(xiàn)來幫助 React 猜對(duì)。
那么菊值?
留個(gè)思考題外驱,vue 是怎么利用虛擬 DOM 的?針對(duì)以上缺點(diǎn)怎么做改進(jìn)腻窒?大家可以去了解一下昵宇。
還想了解更多?
本篇完成耗時(shí) 26 個(gè)番茄鐘( 650分鐘)
我是 FeelsChaotic杭煎,一個(gè)寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛卒落,致力于追求代碼優(yōu)雅羡铲、架構(gòu)設(shè)計(jì)和 T 型成長。
歡迎關(guān)注 FeelsChaotic 的簡書和掘金儡毕,如果我的文章對(duì)你哪怕有一點(diǎn)點(diǎn)幫助也切,歡迎 ??!你的鼓勵(lì)是我寫作的最大動(dòng)力腰湾!
最最重要的雷恃,請(qǐng)給出你的建議或意見,有錯(cuò)誤請(qǐng)多多指正费坊!