了解過react的都必定會知道 virtual DOM 的存在,不夸張的說,virtual DOM 就是 react 最核心的技術。virtual DOM 就如一個征戰南北的猛將,為王打下了如今前端領域的半片江山。如果你想了解 react 王朝,那么必須先了解 virtual DOM,了解這個王朝幾乎所有的生命力和戰斗力所在。
有人會覺得我夸張了,認為即使不懂 virtual DOM,也照樣可以用react來開發應用。是的沒錯,但這并不能否認 virtual DOM 的重要性,事實上,你使用 react 的時候,之所以能夠得心應手地開發著大型應用,而不用瞻前顧后地考慮著性能問題,功勞依然來自于 virtual DOM。
virtual DOM
性能
大多數人對 virtual DOM 的認知,不外乎:
- 在瀏覽器內存中維護著的一棵與頁面 DOM 結構一致的對象樹
- 依靠 diff 算法極大提高了 DOM 操作的性能
但并不知道 virtual DOM 是如何提高性能的。我們暫且拋開這個問題,先來了解一下瀏覽器是怎么將DOM反映到頁面上的。
瀏覽器工作流
創建 DOM 樹
一旦瀏覽器接收到一個 HTML 文件,渲染引擎(render engine)就開始解析它,并根據 HTML 元素(elements)一一對應地生成 DOM 節點(nodes),組成一棵 DOM 樹。
創建渲染樹
同時,瀏覽器也會解析來自外部 CSS 文件和元素上的 inline 樣式。在這個過程中,瀏覽器會逐步對各個節點計算最終樣式,并為包含樣式信息的 DOM 樹上的節點,再創建另外一個樹,一般被稱作渲染樹(render tree)。
布局
構造了渲染樹以后,瀏覽器引擎開始著手布局(layout)。布局時,渲染樹上的每個節點根據其在屏幕上應該出現的精確位置,分配一組屏幕坐標值。
繪制
接著,瀏覽器將會通過遍歷渲染樹,調用每個節點的 paint 方法來繪制節點在渲染樹創建階段返回的 render 對象。通過繪制,最終將在屏幕上展示內容。
性能瓶頸
從上邊瀏覽器的工作流可以看出,每一次的 DOM 操作,都會引發一次從創建 DOM 樹、創建渲染樹、布局到繪制的全過程,尤其是在創建渲染樹階段,對節點樣式的計算量通常很大。而正常的,由用戶引發的頁面改變往往不止一次的 DOM 操作,多次計算,將導致頁面性能大幅降低。
virtual DOM 做了什么
通過分析,我們可以很清楚的意識到,多次的 DOM 操作引發的多次計算,是導致頁面性能低的主要原因。而 virtual DOM 的解決方法很簡單,批量處理 DOM 操作。virtual DOM 實際上是起了一個緩沖的作用,它將一個事件循環(event loop)中發出的 DOM 操作全部收集起來,不立即在頁面上產生效果,而是在事件循環的結尾,才向頁面作用,從而合并多次的 DOM 操作為一次計算。
在此基礎上,virtual DOM 通過 Diff 算法,以優化的策略計算出最小的差別,并作用到真實的DOM上。
通過合并 DOM 操作和diff算法,virtual DOM 有效地解決了 DOM 操作所帶來的性能問題,使得 react 在開發大型復雜的單頁面應用中脫穎而出,大放異彩。
獨特的Diff算法
為什么還需要 Diff 算法呢?這是因為在 web 頁面中,DOM 樹結構通常比較穩定,對于其中某個或某幾個 DOM 節點的修改,沒必要重新創建一顆 DOM 樹。通過 Diff 算法,react將一次計算中多余的渲染工作盡最大化去除,從而進一步提升了頁面性能。
實際上,Diff 算法不是react首創,但卻是在 react 這里得到了突破性的優化。傳統標準的 Diff 算法復雜度達到了 O(n^3),這就意味著,如果要展示1000個節點,就要依次執行上十億次的比較。這是絕對無法滿足性能需求的。而 react 開發團隊通過制定大膽的策略,使得 Diff 算法復雜度降到 O(n)。
策略之所以大膽,是因為算法有所冒險。react的Diff算法是基于以下三個現實策略進行優化的:
1、Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計;
2、擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構;
3、對于同一層級的一組子節點,它們可以通過唯一 id 進行區分。
基于以上三個前提策略,React 分別對 tree diff、component diff 以及 element diff 進行算法優化,事實也證明這三個前提策略是合理且準確的,它保證了整體界面構建的性能。
tree diff
基于策略一,React 對樹的算法進行了簡潔明了的優化,即對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。
既然 DOM 節點跨層級的移動操作少到可以忽略不計,針對這一現象,React 通過對 Virtual DOM 樹進行層級控制,只會對同一個父節點下的所有子節點進行比較。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
當然,這個策略的風險性就在于,當發生 DOM 節點跨層級的移動操作時,react的處理方式將極其殘暴,他會先創建新的節點,再刪除原來需要移動的節點。因此,react 官方也建議不要進行 DOM 節點跨層級的操作。
component diff
React 是基于組件構建應用的,對于組件間的比較所采取的策略也是簡潔高效。
- 如果是同一類型的組件,按照原策略繼續比較 virtual DOM tree。
- 如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節點。
- 對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進行 diff,而這個 API 也成為了 react 性能優化的常見手段。
element diff
當節點處于同一層級時,React diff 提供了三種節點操作,分別為:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。
- INSERT_MARKUP,新的 component 類型不在老集合里, 即是全新的節點,需要對新節點執行插入操作。
- MOVE_EXISTING,在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種情況下 prevChild=nextChild,就需要做移動操作,可以復用以前的 DOM 節點。
- REMOVE_NODE,老 component 類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執行刪除操作,或者老 component 不在新集合里的,也需要執行刪除操作。
值得注意的是,react的性能優化并不是什么神秘的事,任何項目都可以運用類似方法去改善頁面性能,只不過,react幫你做了這些繁瑣的工作。
抽象
到此,我們了解了 virtual DOM 對性能的優化方案,也足以意識到高性能作為 react 的王牌優勢,virtual DOM 在其中所扮演的重要角色。但是如果談到 virtual DOM,你只想起 “提升性能” 這一個關鍵詞的話,那就說明你對 virtual DOM 還不夠了解,事實上,virtual DOM 最創造性最顛覆式的意義,在于抽象。
我們知道,virtual DOM 對真實 DOM 進行了一層抽象,它幫助我們去操作真實 DOM,而我們通過操作 virtual DOM 來控制頁面 UI。在使用 react 之前,我們的 js 代碼和 UI 是完全耦合的。但是 virtual DOM 強制在邏輯代碼和 UI 成分之間構建了一層隔離,使得邏輯和視圖低耦化,極大提高了代碼復用性。于是我們發現,一套邏輯,可以對應多個 UI。這對于代碼移植和維護是具有重大意義的。react native 的推出,完全說明了 virtual DOM 的顛覆性意義。
上圖中,virtual DOM可以映射到web端、IOS端、安卓平臺,在不同環境下的不同表現,均使用了同一套業務邏輯。
這才是 virtual DOM 的靈魂所在,正如 react native 的理念——“Learn Once ,Write Anywhere”。 其實在 Vue 、Angular 2相繼推出后,react 的性能優勢已慢慢不再明顯,但是 virtual DOM 的革命性意義,依然保持著 react 在前端領域中不可撼動的地位,保 react 王朝生生不息。
末尾
在寫這篇博文之前,我已經在許多項目中反復地使用過 react 了,但我最近在思考,我到底了解不了解它。當然,答案的確認花不了三秒:不。甚至是一概不知。心血來潮地翻閱了很多疑惑之處,自覺醍醐灌頂,也分享給正在路上的各位。
參考資料
http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react/
http://blog.csdn.net/lihongxun945/article/details/46640503
http://blog.csdn.net/yczz/article/details/49886061
http://www.tuicool.com/articles/Ar6Zruq
http://www.cnblogs.com/mooniitt/p/6064749.html