虚拟DOM Diff算法解析
React中最神奇的部分莫過于虛擬DOM,以及其高效的Diff算法。這讓我們可以無需擔(dān)心性能問題而”毫無顧忌”的隨時(shí)“刷新”整個(gè)頁面,由虛擬DOM來確保只對界面上真正變化的部分進(jìn)行實(shí)際的DOM操作。React在這一部分已經(jīng)做到足夠透明,在實(shí)際開發(fā)中我們基本無需關(guān)心虛擬DOM是如何運(yùn)作的。然而,作為有態(tài)度的程序員,我們總是對技術(shù)背后的原理充滿著好奇。理解其運(yùn)行機(jī)制不僅有助于更好的理解React組件的生命周期,而且對于進(jìn)一步優(yōu)化React程序也會有很大幫助。
什么是DOM Diff算法
Web界面由DOM樹來構(gòu)成,當(dāng)其中某一部分發(fā)生變化時(shí),其實(shí)就是對應(yīng)的某個(gè)DOM節(jié)點(diǎn)發(fā)生了變化。在React中,構(gòu)建UI界面的思路是由當(dāng)前狀態(tài)決定界面。前后兩個(gè)狀態(tài)就對應(yīng)兩套界面,然后由React來比較兩個(gè)界面的區(qū)別,這就需要對DOM樹進(jìn)行Diff算法分析。
即給定任意兩棵樹,找到最少的轉(zhuǎn)換步驟。但是標(biāo)準(zhǔn)的的Diff算法復(fù)雜度需要O(n^3),這顯然無法滿足性能要求。要達(dá)到每次界面都可以整體刷新界面的目的,勢必需要對算法進(jìn)行優(yōu)化。這看上去非常有難度,然而Facebook工程師卻做到了,他們結(jié)合Web界面的特點(diǎn)做出了兩個(gè)簡單的假設(shè),使得Diff算法復(fù)雜度直接降低到O(n)
兩個(gè)相同組件產(chǎn)生類似的DOM結(jié)構(gòu),不同的組件產(chǎn)生不同的DOM結(jié)構(gòu);
對于同一層次的一組子節(jié)點(diǎn),它們可以通過唯一的id進(jìn)行區(qū)分。
算法上的優(yōu)化是React整個(gè)界面Render的基礎(chǔ),事實(shí)也證明這兩個(gè)假設(shè)是合理而精確的,保證了整體界面構(gòu)建的性能。
不同節(jié)點(diǎn)類型的比較
為了在樹之間進(jìn)行比較,我們首先要能夠比較兩個(gè)節(jié)點(diǎn),在React中即比較兩個(gè)虛擬DOM節(jié)點(diǎn),當(dāng)兩個(gè)節(jié)點(diǎn)不同時(shí),應(yīng)該如何處理。這分為兩種情況:(1)節(jié)點(diǎn)類型不同 ,(2)節(jié)點(diǎn)類型相同,但是屬性不同。本節(jié)先看第一種情況。
當(dāng)在樹中的同一位置前后輸出了不同類型的節(jié)點(diǎn),React直接刪除前面的節(jié)點(diǎn),然后創(chuàng)建并插入新的節(jié)點(diǎn)。假設(shè)我們在樹的同一位置前后兩次輸出不同類型的節(jié)點(diǎn)。
renderB:?<span?/>
=>?[removeNode?<div?/>],?[insertNode?<span?/>]
當(dāng)一個(gè)節(jié)點(diǎn)從div變成span時(shí),簡單的直接刪除div節(jié)點(diǎn),并插入一個(gè)新的span節(jié)點(diǎn)。這符合我們對真實(shí)DOM操作的理解。
需要注意的是,刪除節(jié)點(diǎn)意味著徹底銷毀該節(jié)點(diǎn),而不是再后續(xù)的比較中再去看是否有另外一個(gè)節(jié)點(diǎn)等同于該刪除的節(jié)點(diǎn)。如果該刪除的節(jié)點(diǎn)之下有子節(jié)點(diǎn),那么這些子節(jié)點(diǎn)也會被完全刪除,它們也不會用于后面的比較。這也是算法復(fù)雜能夠降低到O(n)的原因。
上面提到的是對虛擬DOM節(jié)點(diǎn)的操作,而同樣的邏輯也被用在React組件的比較,例如:
renderB:?<Content?/>
=>?[removeNode?<Header?/>],?[insertNode?<Content?/>]
當(dāng)React在同一個(gè)位置遇到不同的組件時(shí),也是簡單的銷毀第一個(gè)組件,而把新創(chuàng)建的組件加上去。這正是應(yīng)用了第一個(gè)假設(shè),不同的組件一般會產(chǎn)生不一樣的DOM結(jié)構(gòu),與其浪費(fèi)時(shí)間去比較它們基本上不會等價(jià)的DOM結(jié)構(gòu),還不如完全創(chuàng)建一個(gè)新的組件加上去。
由這一React對不同類型的節(jié)點(diǎn)的處理邏輯我們很容易得到推論,那就是React的DOM Diff算法實(shí)際上只會對樹進(jìn)行逐層比較,如下所述。
逐層進(jìn)行節(jié)點(diǎn)比較
提到樹,相信大多數(shù)同學(xué)立刻想到的是二叉樹,遍歷,最短路徑等復(fù)雜的數(shù)據(jù)結(jié)構(gòu)算法。而在React中,樹的算法其實(shí)非常簡單,那就是兩棵樹只會對同一層次的節(jié)點(diǎn)進(jìn)行比較。如下圖所示:
React只會對相同顏色方框內(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)會被完全刪除掉,不會用于進(jìn)一步的比較。這樣只需要對樹進(jìn)行一次遍歷,便能完成整個(gè)DOM樹的比較。
例如,考慮有下面的DOM結(jié)構(gòu)轉(zhuǎn)換:
A節(jié)點(diǎn)被整個(gè)移動到D節(jié)點(diǎn)下,直觀的考慮DOM Diff操作應(yīng)該是
D.append(A);
但因?yàn)镽eact只會簡單的考慮同層節(jié)點(diǎn)的位置變換,對于不同層的節(jié)點(diǎn),只有簡單的創(chuàng)建和刪除。當(dāng)根節(jié)點(diǎn)發(fā)現(xiàn)子節(jié)點(diǎn)中A不見了,就會直接銷毀A;而當(dāng)D發(fā)現(xiàn)自己多了一個(gè)子節(jié)點(diǎn)A,則會創(chuàng)建一個(gè)新的A作為子節(jié)點(diǎn)。因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實(shí)際操作是:
可以看到,以A為根節(jié)點(diǎn)的樹被整個(gè)重新創(chuàng)建。
雖然看上去這樣的算法有些“簡陋”,但是其基于的是第一個(gè)假設(shè):兩個(gè)不同組件一般產(chǎn)生不一樣的DOM結(jié)構(gòu)。根據(jù)React官方博客,這一假設(shè)至今為止沒有導(dǎo)致嚴(yán)重的性能問題。這當(dāng)然也給我們一個(gè)提示,在實(shí)現(xiàn)自己的組件時(shí),保持穩(wěn)定的DOM結(jié)構(gòu)會有助于性能的提升。例如,我們有時(shí)可以通過CSS隱藏或顯示某些節(jié)點(diǎn),而不是真的移除或添加DOM節(jié)點(diǎn)。
由DOM Diff算法理解組件的生命周期
在上一篇文章中介紹了React組件的生命周期,其中的每個(gè)階段其實(shí)都是和DOM Diff算法息息相關(guān)的。例如以下幾個(gè)方法:
constructor: 構(gòu)造函數(shù),組件被創(chuàng)建時(shí)執(zhí)行;
componentDidMount: 當(dāng)組件添加到DOM樹之后執(zhí)行;
componentWillUnmount: 當(dāng)組件從DOM樹中移除之后執(zhí)行,在React中可以認(rèn)為組件被銷毀;
componentDidUpdate: 當(dāng)組件更新時(shí)執(zhí)行。
為了演示組件生命周期和DOM Diff算法的關(guān)系,筆者創(chuàng)建了一個(gè)示例:https://supnate.github.io/react-dom-diff/index.html?,大家可以直接訪問試用。這時(shí)當(dāng)DOM樹進(jìn)行如下轉(zhuǎn)變時(shí),即從“shape1”轉(zhuǎn)變到“shape2”時(shí)。我們來觀察這幾個(gè)方法的執(zhí)行情況:
瀏覽器開發(fā)工具控制臺輸出如下結(jié)果:
C?is?created.
B?is?updated.
A?is?updated.
C?did?mount.
D?is?updated.
R?is?updated.
可以看到,C節(jié)點(diǎn)是完全重建后再添加到D節(jié)點(diǎn)之下,而不是將其“移動”過去。如果大家有興趣,也可以fork示例代碼:https://github.com/supnate/react-dom-diff?。從而可以自己添加其它樹結(jié)構(gòu),試驗(yàn)它們之間是如何轉(zhuǎn)換的。
相同類型節(jié)點(diǎn)的比較
第二種節(jié)點(diǎn)的比較是相同類型的節(jié)點(diǎn),算法就相對簡單而容易理解。React會對屬性進(jìn)行重設(shè)從而實(shí)現(xiàn)節(jié)點(diǎn)的轉(zhuǎn)換。例如:
renderB:?<div?id="after"?/>
=>?[replaceAttribute?id?"after"]
虛擬DOM的style屬性稍有不同,其值并不是一個(gè)簡單字符串而必須為一個(gè)對象,因此轉(zhuǎn)換過程如下:
renderB:?<div?style={{fontWeight:?'bold'}}?/>
=>?[removeStyle?color],?[addStyle?font-weight?'bold']
列表節(jié)點(diǎn)的比較
上面介紹了對于不在同一層的節(jié)點(diǎn)的比較,即使它們完全一樣,也會銷毀并重新創(chuàng)建。那么當(dāng)它們在同一層時(shí),又是如何處理的呢?這就涉及到列表節(jié)點(diǎn)的Diff算法。相信很多使用React的同學(xué)大多遇到過這樣的警告:
這是React在遇到列表時(shí)卻又找不到key時(shí)提示的警告。雖然無視這條警告大部分界面也會正確工作,但這通常意味著潛在的性能問題。因?yàn)镽eact覺得自己可能無法高效的去更新這個(gè)列表。
列表節(jié)點(diǎn)的操作通常包括添加、刪除和排序。例如下圖,我們需要往B和C直接插入節(jié)點(diǎn)F,在jQuery中我們可能會直接使用$(B).after(F)來實(shí)現(xiàn)。而在React中,我們只會告訴React新的界面應(yīng)該是A-B-F-C-D-E,由Diff算法完成更新界面。
這時(shí)如果每個(gè)節(jié)點(diǎn)都沒有唯一的標(biāo)識,React無法識別每一個(gè)節(jié)點(diǎn),那么更新過程會很低效,即,將C更新成F,D更新成C,E更新成D,最后再插入一個(gè)E節(jié)點(diǎn)。效果如下圖所示:
可以看到,React會逐個(gè)對節(jié)點(diǎn)進(jìn)行更新,轉(zhuǎn)換到目標(biāo)節(jié)點(diǎn)。而最后插入新的節(jié)點(diǎn)E,涉及到的DOM操作非常多。而如果給每個(gè)節(jié)點(diǎn)唯一的標(biāo)識(key),那么React能夠找到正確的位置去插入新的節(jié)點(diǎn),入下圖所示:
對于列表節(jié)點(diǎn)順序的調(diào)整其實(shí)也類似于插入或刪除,下面結(jié)合示例代碼我們看下其轉(zhuǎn)換的過程。仍然使用前面提到的示例:https://supnate.github.io/react-dom-diff/index.html?,我們將樹的形態(tài)從shape5轉(zhuǎn)換到shape6:
即將同一層的節(jié)點(diǎn)位置進(jìn)行調(diào)整。如果未提供key,那么React認(rèn)為B和C之后的對應(yīng)位置組件類型不同,因此完全刪除后重建,控制臺輸出如下:
C?will?unmount.
C?is?created.
B?is?created.
C?did?mount.
B?did?mount.
A?is?updated.
R?is?updated.
而如果提供了key,如下面的代碼:
??return?(????<Root>
??????<A>
????????<B?key="B"?/>
????????<C?key="C"?/>
??????</A>
????</Root>
??);
},
shape6:?function()?{
??return?(????<Root>
??????<A>
????????<C?key="C"?/>
????????<B?key="B"?/>
??????</A>
????</Root>
??);
},
那么控制臺輸出如下:
B?is?updated.
A?is?updated.
R?is?updated.
可以看到,對于列表節(jié)點(diǎn)提供唯一的key屬性可以幫助React定位到正確的節(jié)點(diǎn)進(jìn)行比較,從而大幅減少DOM操作次數(shù),提高了性能。
小結(jié)
本文分析了React的DOM Diff算法究竟是如何工作的,其復(fù)雜度控制在了O(n),這讓我們考慮UI時(shí)可以完全基于狀態(tài)來每次render整個(gè)界面而無需擔(dān)心性能問題,簡化了UI開發(fā)的復(fù)雜度。而算法優(yōu)化的基礎(chǔ)是文章開頭提到的兩個(gè)假設(shè),以及React的UI基于組件這樣的一個(gè)機(jī)制。理解虛擬DOM Diff算法不僅能夠幫助我們理解組件的生命周期,而且也對我們實(shí)現(xiàn)自定義組件時(shí)如何進(jìn)一步優(yōu)化性能具有指導(dǎo)意義。
本文摘自異步社區(qū),發(fā)表人:?xiangzhihong?,作品:《虛擬DOM Diff算法解析》,未經(jīng)授權(quán),禁止轉(zhuǎn)載。
推薦閱讀
2018年5月新書書單(文末福利)
2018年4月新書書單
異步圖書最全Python書單
一份程序員必備的算法書單
第一本Python神經(jīng)網(wǎng)絡(luò)編程圖書
長按二維碼,可以關(guān)注我們喲
每天與你分享IT好文。
在“異步圖書”后臺回復(fù)“關(guān)注”,即可免費(fèi)獲得2000門在線視頻課程
點(diǎn)擊查看原文,內(nèi)容
閱讀原文
轉(zhuǎn)載于:https://blog.51cto.com/13127751/2134222
總結(jié)
以上是生活随笔為你收集整理的虚拟DOM Diff算法解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GDB 配置
- 下一篇: Optaplanner规划引擎的工作原理