从各大跨平台技术说开去,我们真的需要虚拟 DOM 吗?
前言
你有沒有留意到?優秀的解決方案思想都是相通的:當你研究 Flutter 渲染原理時會發現 Flutter Rendering 層類似于 React 中的虛擬 DOM,當你去看 Weex 工作原理時,誒,又發現了虛擬 DOM 的身影,更別提 VUE 響應式視圖的核心也是虛擬 DOM 了。
那這個虛擬 DOM 有什么用?為什么這么多框架都應用了它?本質上帶來了什么優勢?本文將結合前端和移動端來談談。
什么是 DOM?什么是虛擬 DOM?
DOM 就是文檔樹,與用戶界面控件樹對應,在 web 開發中通常指 HTML 對應的渲染樹,但廣義的 DOM 也可以指 Android 中的 XML 布局對應的控件樹,而 DOM 操作就是指直接操作渲染樹(或控件樹)。
虛擬 DOM 是一個用來表示真實的 DOM 結構的數據結構。
思考
想當年學前端的時候,還是 jQuery 的時代,想賦值?改個樣式?取值?都是document.getElementById()咔咔一頓操作。這樣直接操作 DOM 會有什么問題?
直接操作 DOM 帶來的問題
1. model 和 view 耦合
最直觀的問題之一, 把用戶請求的表現邏輯和控制層要實現的業務邏輯兩者混合起來了,兩部分的依賴非常強。
2. 高頻操作引起性能損耗
寫個簡單Demo,我們看下效果。
為什么會有性能損耗?
原因可以歸結為 2 點:
1. 跨界交流損耗
把 DOM 和 ECMAScript 各自想象成一個島嶼,它們之間用收費橋梁連接。 ????????????????????????????????????????????????????????????????????????????????——《高性能JavaScript》
DOM 屬于渲染引擎,而 JS 又是屬于 JS 引擎,在瀏覽器內核中他們彼此獨立。單獨來看,兩者都是很快的,但當我們用 JS 去操作 DOM 時,引擎之間進行了“跨界交流”。這個“跨界交流”的實現并不簡單,它依賴了橋接接口作為“橋梁”,如下圖:
既然是收費橋梁,過“橋”就要收費。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值),都要過一次“橋”。次數一多就會產生比較明顯的性能問題。
那移動端混合開發的情況呢?
就拿 RectNative 舉例,RectNative 是一套 UI 基于原生控件(非Web UI)業務邏輯基于 JS 的跨平臺技術解決方案,JS 中所寫控件標簽不是真實控件,會在 Native 端解析為原生控件,如<Text>標簽對應 Android 中的 TextView 控件。
在布局過程中 RN 需要在 JS 和 Native 之間通信,如果遇到滑動和拖動的情況,劣勢就很明顯了,這和在瀏覽器中要 JS 頻繁操作 DOM 所帶來的問題是相同的,都會帶來比較可觀的性能開銷。
2. DOM 修改引起重繪或重排
修改 DOM 屬性的代價更是昂貴,它會導致渲染引擎重新計算幾何變化(重排和重繪)。我們來看下渲染步驟:
在頁面生成時,至少會進行一次布局和渲染,后面用戶操作時,如果修改了 DOM 節點,會觸發渲染樹(Render Tree)的變化,從而進行上圖的步驟2、3、4、5,因此如果在 js 中存在很多 DOM 操作,就會不斷地觸發重繪或重排,影響頁面性能。
在移動端,情況也好不到哪里去。
布局中的任何一個 View 一旦發生屬性變化,都可能引起很大的連鎖反應(如果所在的控件層級非常復雜的話)。例如某個 btn 的大小突然增加一倍,有可能會導致兄弟視圖的位置變化,也有可能導致父視圖的大小發生改變。當大量的 layout() 操作被頻繁調用執行時,會引起整個 View 頻繁地重渲,最終導致丟幀或 UI 卡頓。
解決辦法
針對以上的問題,我們一一提出解決方案:
1. 減少跨界過橋次數,合并操作
ECMAScript 每次訪問 DOM,都要經過這座橋,并交納“過橋費”,訪問 DOM 的次數越多,費用也就越高。因此,推薦的做法是盡量減少過橋的次數,努力呆在 ECMAScript 島上。???????????????????????????????????????????????????——《高性能JavaScript》
我們來分析下,怎么減少“過橋的次數”?過橋次數之所以頻繁,和頻繁的 DOM 操作有關。
比如我們給列表加數據,最差的方式就是這樣:
for (var i = 0; i < N; i++) {var li = document.createElement("li");li.innerHTML = arr[i];ul.appendChild(li);} 復制代碼這里會操作 N 次 DOM 觸發 N 次重繪。重渲肯定是無法避免的,我們的目標是最小化重繪和重排次數。
那能不能不要立即去操作 DOM 呢?
將這 N 次更新的內容保存到一個 js 對象中,最終將這個 js 對象一次性 attach 到 DOM 樹上,通知瀏覽器去執行繪制工作。這樣無論多么復雜的 DOM 操作,最終都只會觸發一次渲染全流程,避免了大量的無謂計算量,這樣不就可以了么!(欣喜若狂.jpg)
但優化 DOM 操作方式很多,不一定要依賴虛擬 DOM,所以這不是我們需要虛擬 DOM 的根本原因,根本的原因還是響應式需求。
2. 響應式
如果通過 JS 直接操作 DOM 的話,勢必會造成視圖數據和模型數據的不匹配,我們能不能讓開發者只關心狀態(數據)變化,而無需關心控件操作呢?當然可以!
React 中提出一個重要思想:狀態改變則 UI 隨之自動改變。
每次狀態有變動就重構用戶界面,重渲整個 view。如果沒有虛擬 DOM,簡單粗暴的做法就是直接重置 innerHTML,在大部分數據都變了的情況下,重置 innerHTML 還算合理,但如果只有一行數據變了,顯然就有大量的浪費。
這是我們需要虛擬 DOM 的原因,用它來代替開發者的手工操作,確保只對真正有變化的部分進行實際的 DOM 操作(局部刷新)。
3. 總結
開發者對數據和狀態所做的任何改動,都會被自動且高效的同步到虛擬 DOM(自動同步,體現響應式),最后再批量同步到真實 DOM 中,而不是每次改變都去操作一下 DOM(批量同步,體現合并操作)
怎么利用虛擬 DOM?
1. React
當 React UI 渲染時,先渲染一個虛擬 DOM,這是一個輕量的純 js 的對象結構,并沒有完全實現 DOM,最主要的還是保留了節點之間的層次關系和一些基本屬性,因為 DOM 實在是太復雜,實際在做最后繪制時,這些都是不需要關心的。所以虛擬 DOM 里每一個節點只有幾個簡單屬性,哪怕是直接把虛擬 DOM 刪了,根據新傳進來的數據重新創建一個新的虛擬 DOM 都非常快。
當有變化時,生成一個新的虛擬 DOM。這個新的虛擬DOM反應了數據模型的新狀態。現在我們有 2 個虛擬DOM:新的和老的。對比 DOM 樹差異得到一個 Patch,把這個 Patch 打到真實的 DOM 上去,這有點像版本控制打patch的思路。
那我們怎么比較出兩顆 DOM 樹的差異呢? Diff 算法!
即給定任意兩棵樹,找到最少的轉換步驟。但是標準的 Diff 算法復雜度需要 O(n^3),這顯然無法滿足性能要求。Facebook 工程師結合 Web 界面的特點做出了兩個簡單的假設,使得 Diff 算法復雜度直接降低到 O(n)。
算法上的優化是 React 整個界面 Render 的基礎,事實也證明這兩個假設是合理而精確的,保證了整體界面構建的性能。
由這一對不同類型的節點的處理邏輯我們很容易得到推論,那就是 React 的 DOM Diff 算法實際上只會對樹進行逐層比較,如下圖:
React 只會對相同顏色方框內的 DOM 節點進行比較,即同一個父節點下的所有子節點。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
實際實踐起來,Diff 算法并沒有這么簡單,感興趣的小伙伴可以在文末的推文去深入了解。
那跨平臺方案的情況呢?
2. RN
上文已經提到 RN 是 React 在原生移動應用平臺的衍生產物,那兩者主要的區別是什么呢?主要的區別在于虛擬 DOM 映射的對象是什么。React 中虛擬 DOM 最終會映射為瀏覽器 DOM 樹,而 RN 中虛擬 DOM 會通過 JavaScriptCore 映射為原生控件樹。
步驟如下:
至此,RN 便實現了跨平臺。
3. weex
weex 一定程度上用 JS 實現了 vue 一統天下的效果。
可以看到,weex 會編譯構建虛擬 DOM,并發送渲染指令給 RenderEngine 層,這樣,同樣一份 JSON 數據,在不同平臺的渲染引擎中能夠渲染成不同版本的 UI,這是 Weex 可以實現動態化的原因。
那三端的語法都不一樣,Weex是怎么統一的?重點在于 JS Framework!
weex 在 RN 的 JS V8 引擎基礎上,多了 JS Framework 承當了重要的職責,它主要負責:管理 Weex 的生命周期;解析 JS Bundle,轉為 Virtual DOM,再通過所在平臺不同的 API 構建頁面;進行雙向的數據交互和響應。
這使得上層具備統一性,在開發過程中,代碼模式、編譯過程、模板組件、數據綁定、生命周期等上層語法是一致的。得益于上層的統一,只需要在 JS Framework 層的最后判斷是由 Vue.js 生成真實的 DOM,還是通過 Native Api 渲染組件即可。
4. Flutter
RN 和 React 原理相通,那 Flutter 呢?Flutter Widget 的中心思想是用 Widget 構建你的UI(非原生控件)。 那少了原生控件層和 js 層的通信損耗,不需要用虛擬 DOM 了吧?
非也! Flutter Widget 從 React 中獲得了靈感,也是采用現代響應式框架構建。
先看看 Flutter 中三顆重要的樹:
-
Widget 樹:控件樹,表示了我們在 dart 代碼中所寫的控件的結構,但這只是描述信息,渲染引擎是不認識的。
Widget 被開發人員配置了多個屬性來定義它的展現形式,例如配置 Text 組件需要顯示的字符串,配置輸入框組件需要顯示的內容……Element 樹會記錄這些配置信息。
-
Element 數:實際控件樹
在手機屏幕上顯示的控件并非我們在代碼中所寫的 Widget,Flutter 會根據 Widget 樹信息生成控件對應的 Element 樹,在 Flutter 中,一個 Widget 通過多次復用可以對應多個 Element 實例,Element 才是我們真正在屏幕上顯示的元素。
Element 與 Widget 另一個區別在于,Widget 是不可變的,它的改變就意味著要重建,而其重建也非常頻繁,如果我們將更多的任務交給它,將會對性能造成很大的損耗,因此我們把 Widget 樹當作一個虛擬 DOM 樹,真正被渲染在屏幕上的其實是 ElememtTree,它持有其對應 Widget 的引用,如果對應的 Widget 發生改變,它就會被標記為 dirty Element,下一次更新視圖時根據這個狀態只更新被修改的內容,這樣就把可變狀態與 Widget 關聯起來,從而達到提升性能的效果。
-
RenderObject 樹:渲染樹,做組件布局渲染工作,包含渲染搭配、布局約束等信息。
簡而言之,Flutter 引入虛擬 DOM 的目的是為了確定底層渲染樹從一個狀態轉換到下一個狀態所需的最小更改。
虛擬 DOM 對跨平臺技術的意義
那分析完各種跨平臺技術,你對虛擬 DOM 有了怎樣的認識了呢?
為什么使用虛擬 DOM?
是因為快?(實際上不一定快)
是因為解耦?
是因為響應式?
對跨平臺技術來說,更重要的意義在于:
虛擬 DOM 是 DOM 在內存中的一種輕量級表達方式,是一種統一約定!可以通過不同的渲染引擎生成不同平臺下的 UI!
虛擬 DOM 的可移植性非常好,這意味著可以渲染到 DOM 以外的任何端,發揮你的想象力,可以做的事情很多。
再次審視虛擬 DOM
虛擬 DOM 真正的價值從來都不是性能,而是不管數據怎么變化,都可以用最小的代價來更新 DOM,而且掩蓋了底層的 DOM 操作,讓你用更聲明式的方式來描述你的目的,從而讓你的代碼更容易維護。
虛擬 DOM 帶來了很多好思路,打開了通向有趣架構的大門,例如將視圖視為狀態函數。它讓我們編寫代碼,就像重新呈現整個場景一樣。這不禁讓我感慨,沒有什么是加中間件不能解決的,如果有,那就再加多個中間件。
5 個詞語概括下意義:
可維護性、最小的代價、效率、函數式UI、數據驅動
進一步思考
虛擬 DOM 的說明已經結束了,但是對于虛擬 DOM 的思考遠沒有結束。
Rect 的方式有兩大缺點:
每次數據更改,哪怕改動很小,都會生成完整的虛擬 DOM,如果 DOM 很復雜,這個過程就會白白浪費很多計算資源;
比較虛擬 DOM 差異的過程,既慢又容易出錯。因為 React 持有的新舊虛擬 DOM 相互獨立,React 并不知道數據源發生了什么操作,只能根據兩個虛擬 DOM 來猜測需要執行的操作。自動的猜測算法既不準又慢,必須要前端開發者手動提供 key 屬性和一些額外的方法實現來幫助 React 猜對。
那么?
留個思考題,vue 是怎么利用虛擬 DOM 的?針對以上缺點怎么做改進?大家可以去了解一下。
還想了解更多?
- 別再說虛擬 DOM 快了,要被打臉的
- 網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什么?
- 虛擬DOM和Diff算法 - 入門級
- React的虛擬DOM與diff算法的理解
- 深度剖析:如何實現一個 Virtual DOM 算法
- 詳解vue的diff算法
- 解析vue2.0的diff算法
本篇完成耗時 26 個番茄鐘( 650分鐘)
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力于追求代碼優雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,如果我的文章對你哪怕有一點點幫助,歡迎 ??!你的鼓勵是我寫作的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!
轉載于:https://juejin.im/post/5ce7e8fb51882555003dceef
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的从各大跨平台技术说开去,我们真的需要虚拟 DOM 吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Git pull[push] 不用每次输
- 下一篇: MarkdownPad2基础语法