了解過react的都必定會(huì)知道 virtual DOM 的存在熔恢,不夸張的說个粱,virtual DOM 就是 react 最核心的技術(shù)撵彻。virtual DOM 就如一個(gè)征戰(zhàn)南北的猛將墙贱,為王打下了如今前端領(lǐng)域的半片江山艺演。如果你想了解 react 王朝却紧,那么必須先了解 virtual DOM,了解這個(gè)王朝幾乎所有的生命力和戰(zhàn)斗力所在胎撤。
有人會(huì)覺得我夸張了晓殊,認(rèn)為即使不懂 virtual DOM,也照樣可以用react來開發(fā)應(yīng)用伤提。是的沒錯(cuò)巫俺,但這并不能否認(rèn) virtual DOM 的重要性,事實(shí)上肿男,你使用 react 的時(shí)候介汹,之所以能夠得心應(yīng)手地開發(fā)著大型應(yīng)用,而不用瞻前顧后地考慮著性能問題次伶,功勞依然來自于 virtual DOM痴昧。
virtual DOM
性能
大多數(shù)人對(duì) virtual DOM 的認(rèn)知,不外乎:
- 在瀏覽器內(nèi)存中維護(hù)著的一棵與頁面 DOM 結(jié)構(gòu)一致的對(duì)象樹
- 依靠 diff 算法極大提高了 DOM 操作的性能
但并不知道 virtual DOM 是如何提高性能的冠王。我們暫且拋開這個(gè)問題,先來了解一下瀏覽器是怎么將DOM反映到頁面上的舌镶。
瀏覽器工作流
創(chuàng)建 DOM 樹
一旦瀏覽器接收到一個(gè) HTML 文件柱彻,渲染引擎(render engine)就開始解析它,并根據(jù) HTML 元素(elements)一一對(duì)應(yīng)地生成 DOM 節(jié)點(diǎn)(nodes)餐胀,組成一棵 DOM 樹哟楷。
創(chuàng)建渲染樹
同時(shí),瀏覽器也會(huì)解析來自外部 CSS 文件和元素上的 inline 樣式否灾。在這個(gè)過程中卖擅,瀏覽器會(huì)逐步對(duì)各個(gè)節(jié)點(diǎn)計(jì)算最終樣式,并為包含樣式信息的 DOM 樹上的節(jié)點(diǎn),再創(chuàng)建另外一個(gè)樹惩阶,一般被稱作渲染樹(render tree)挎狸。
布局
構(gòu)造了渲染樹以后,瀏覽器引擎開始著手布局(layout)断楷。布局時(shí)锨匆,渲染樹上的每個(gè)節(jié)點(diǎn)根據(jù)其在屏幕上應(yīng)該出現(xiàn)的精確位置,分配一組屏幕坐標(biāo)值冬筒。
繪制
接著恐锣,瀏覽器將會(huì)通過遍歷渲染樹,調(diào)用每個(gè)節(jié)點(diǎn)的 paint 方法來繪制節(jié)點(diǎn)在渲染樹創(chuàng)建階段返回的 render 對(duì)象舞痰。通過繪制土榴,最終將在屏幕上展示內(nèi)容。
性能瓶頸
從上邊瀏覽器的工作流可以看出响牛,每一次的 DOM 操作玷禽,都會(huì)引發(fā)一次從創(chuàng)建 DOM 樹、創(chuàng)建渲染樹娃善、布局到繪制的全過程论衍,尤其是在創(chuàng)建渲染樹階段,對(duì)節(jié)點(diǎn)樣式的計(jì)算量通常很大聚磺。而正常的坯台,由用戶引發(fā)的頁面改變往往不止一次的 DOM 操作,多次計(jì)算瘫寝,將導(dǎo)致頁面性能大幅降低蜒蕾。
virtual DOM 做了什么
通過分析,我們可以很清楚的意識(shí)到焕阿,多次的 DOM 操作引發(fā)的多次計(jì)算咪啡,是導(dǎo)致頁面性能低的主要原因。而 virtual DOM 的解決方法很簡(jiǎn)單暮屡,批量處理 DOM 操作撤摸。virtual DOM 實(shí)際上是起了一個(gè)緩沖的作用,它將一個(gè)事件循環(huán)(event loop)中發(fā)出的 DOM 操作全部收集起來褒纲,不立即在頁面上產(chǎn)生效果准夷,而是在事件循環(huán)的結(jié)尾,才向頁面作用莺掠,從而合并多次的 DOM 操作為一次計(jì)算衫嵌。
在此基礎(chǔ)上,virtual DOM 通過 Diff 算法彻秆,以優(yōu)化的策略計(jì)算出最小的差別楔绞,并作用到真實(shí)的DOM上结闸。
通過合并 DOM 操作和diff算法,virtual DOM 有效地解決了 DOM 操作所帶來的性能問題酒朵,使得 react 在開發(fā)大型復(fù)雜的單頁面應(yīng)用中脫穎而出桦锄,大放異彩。
獨(dú)特的Diff算法
為什么還需要 Diff 算法呢耻讽?這是因?yàn)樵?web 頁面中察纯,DOM 樹結(jié)構(gòu)通常比較穩(wěn)定,對(duì)于其中某個(gè)或某幾個(gè) DOM 節(jié)點(diǎn)的修改针肥,沒必要重新創(chuàng)建一顆 DOM 樹饼记。通過 Diff 算法,react將一次計(jì)算中多余的渲染工作盡最大化去除慰枕,從而進(jìn)一步提升了頁面性能具则。
實(shí)際上,Diff 算法不是react首創(chuàng)具帮,但卻是在 react 這里得到了突破性的優(yōu)化博肋。傳統(tǒng)標(biāo)準(zhǔn)的 Diff 算法復(fù)雜度達(dá)到了 O(n^3),這就意味著蜂厅,如果要展示1000個(gè)節(jié)點(diǎn)匪凡,就要依次執(zhí)行上十億次的比較。這是絕對(duì)無法滿足性能需求的掘猿。而 react 開發(fā)團(tuán)隊(duì)通過制定大膽的策略病游,使得 Diff 算法復(fù)雜度降到 O(n)。
策略之所以大膽稠通,是因?yàn)樗惴ㄓ兴半U(xiǎn)衬衬。react的Diff算法是基于以下三個(gè)現(xiàn)實(shí)策略進(jìn)行優(yōu)化的:
1、Web UI 中 DOM 節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作特別少改橘,可以忽略不計(jì)滋尉;
2、擁有相同類的兩個(gè)組件將會(huì)生成相似的樹形結(jié)構(gòu)飞主,擁有不同類的兩個(gè)組件將會(huì)生成不同的樹形結(jié)構(gòu)狮惜;
3、對(duì)于同一層級(jí)的一組子節(jié)點(diǎn)碌识,它們可以通過唯一 id 進(jìn)行區(qū)分讽挟。
基于以上三個(gè)前提策略,React 分別對(duì) tree diff丸冕、component diff 以及 element diff 進(jìn)行算法優(yōu)化,事實(shí)也證明這三個(gè)前提策略是合理且準(zhǔn)確的薛窥,它保證了整體界面構(gòu)建的性能胖烛。
tree diff
基于策略一眼姐,React 對(duì)樹的算法進(jìn)行了簡(jiǎn)潔明了的優(yōu)化,即對(duì)樹進(jìn)行分層比較佩番,兩棵樹只會(huì)對(duì)同一層次的節(jié)點(diǎn)進(jìn)行比較众旗。
既然 DOM 節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作少到可以忽略不計(jì),針對(duì)這一現(xiàn)象趟畏,React 通過對(duì) Virtual DOM 樹進(jìn)行層級(jí)控制贡歧,只會(huì)對(duì)同一個(gè)父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)進(jì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 樹的比較绍弟。
當(dāng)然,這個(gè)策略的風(fēng)險(xiǎn)性就在于著洼,當(dāng)發(fā)生 DOM 節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作時(shí)樟遣,react的處理方式將極其殘暴,他會(huì)先創(chuàng)建新的節(jié)點(diǎn)身笤,再刪除原來需要移動(dòng)的節(jié)點(diǎn)豹悬。因此,react 官方也建議不要進(jìn)行 DOM 節(jié)點(diǎn)跨層級(jí)的操作液荸。
component diff
React 是基于組件構(gòu)建應(yīng)用的瞻佛,對(duì)于組件間的比較所采取的策略也是簡(jiǎn)潔高效。
- 如果是同一類型的組件莹弊,按照原策略繼續(xù)比較 virtual DOM tree涤久。
- 如果不是,則將該組件判斷為 dirty component忍弛,從而替換整個(gè)組件下的所有子節(jié)點(diǎn)响迂。
- 對(duì)于同一類型的組件,有可能其 Virtual DOM 沒有任何變化细疚,如果能夠確切的知道這點(diǎn)那可以節(jié)省大量的 diff 運(yùn)算時(shí)間蔗彤,因此 React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進(jìn)行 diff,而這個(gè) API 也成為了 react 性能優(yōu)化的常見手段疯兼。
element diff
當(dāng)節(jié)點(diǎn)處于同一層級(jí)時(shí)然遏,React diff 提供了三種節(jié)點(diǎn)操作,分別為:INSERT_MARKUP(插入)吧彪、MOVE_EXISTING(移動(dòng))和 REMOVE_NODE(刪除)待侵。
- INSERT_MARKUP,新的 component 類型不在老集合里姨裸, 即是全新的節(jié)點(diǎn)秧倾,需要對(duì)新節(jié)點(diǎn)執(zhí)行插入操作怨酝。
- MOVE_EXISTING,在老集合有新 component 類型那先,且 element 是可更新的類型农猬,generateComponentChildren 已調(diào)用 receiveComponent,這種情況下 prevChild=nextChild售淡,就需要做移動(dòng)操作斤葱,可以復(fù)用以前的 DOM 節(jié)點(diǎn)。
- REMOVE_NODE揖闸,老 component 類型揍堕,在新集合里也有,但對(duì)應(yīng)的 element 不同則不能直接復(fù)用和更新楔壤,需要執(zhí)行刪除操作鹤啡,或者老 component 不在新集合里的,也需要執(zhí)行刪除操作蹲嚣。
值得注意的是递瑰,react的性能優(yōu)化并不是什么神秘的事,任何項(xiàng)目都可以運(yùn)用類似方法去改善頁面性能隙畜,只不過抖部,react幫你做了這些繁瑣的工作。
抽象
到此议惰,我們了解了 virtual DOM 對(duì)性能的優(yōu)化方案慎颗,也足以意識(shí)到高性能作為 react 的王牌優(yōu)勢(shì),virtual DOM 在其中所扮演的重要角色言询。但是如果談到 virtual DOM俯萎,你只想起 “提升性能” 這一個(gè)關(guān)鍵詞的話,那就說明你對(duì) virtual DOM 還不夠了解运杭,事實(shí)上夫啊,virtual DOM 最創(chuàng)造性最顛覆式的意義,在于抽象辆憔。
我們知道撇眯,virtual DOM 對(duì)真實(shí) DOM 進(jìn)行了一層抽象,它幫助我們?nèi)ゲ僮髡鎸?shí) DOM虱咧,而我們通過操作 virtual DOM 來控制頁面 UI熊榛。在使用 react 之前,我們的 js 代碼和 UI 是完全耦合的腕巡。但是 virtual DOM 強(qiáng)制在邏輯代碼和 UI 成分之間構(gòu)建了一層隔離玄坦,使得邏輯和視圖低耦化,極大提高了代碼復(fù)用性绘沉。于是我們發(fā)現(xiàn)营搅,一套邏輯云挟,可以對(duì)應(yīng)多個(gè) UI。這對(duì)于代碼移植和維護(hù)是具有重大意義的转质。react native 的推出,完全說明了 virtual DOM 的顛覆性意義帖世。
上圖中休蟹,virtual DOM可以映射到web端、IOS端日矫、安卓平臺(tái)赂弓,在不同環(huán)境下的不同表現(xiàn),均使用了同一套業(yè)務(wù)邏輯哪轿。
這才是 virtual DOM 的靈魂所在盈魁,正如 react native 的理念——“Learn Once ,Write Anywhere”。 其實(shí)在 Vue 窃诉、Angular 2相繼推出后杨耙,react 的性能優(yōu)勢(shì)已慢慢不再明顯,但是 virtual DOM 的革命性意義飘痛,依然保持著 react 在前端領(lǐng)域中不可撼動(dòng)的地位珊膜,保 react 王朝生生不息。
末尾
在寫這篇博文之前宣脉,我已經(jīng)在許多項(xiàng)目中反復(fù)地使用過 react 了车柠,但我最近在思考,我到底了解不了解它塑猖。當(dāng)然竹祷,答案的確認(rèn)花不了三秒:不。甚至是一概不知羊苟。心血來潮地翻閱了很多疑惑之處塑陵,自覺醍醐灌頂,也分享給正在路上的各位践险。
參考資料
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