React Diff 算法

0 前言

React的VirtualDOM可謂是前端領(lǐng)域中革命性的創(chuàng)新谢谦,而高效的Diff算法又極大的優(yōu)化了狀態(tài)的對(duì)比,不同的狀態(tài)對(duì)應(yīng)的就是不同的UI界面窟绷。本文將簡(jiǎn)要介紹Diff算法中的精華脖咐。

1 Diff算法

1.1背景

"樹(shù)"是一種常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),對(duì)比"樹(shù)"的結(jié)構(gòu)在計(jì)算機(jī)相關(guān)相關(guān)領(lǐng)域有廣泛的應(yīng)用拂酣。具體到前端開(kāi)發(fā)而言,Web的UI界面就是由DOM樹(shù)構(gòu)成仲义,DOM作為一種有序樹(shù)結(jié)構(gòu)婶熬,當(dāng)DOM樹(shù)結(jié)構(gòu)發(fā)生變化時(shí),瀏覽器會(huì)重新解析渲染DOM樹(shù)埃撵。造成有序樹(shù)的節(jié)點(diǎn)變化的基本操作方式有三種:重標(biāo)記(relabel)赵颅,刪除(delete)和插入(insert),如下圖自上而下所示暂刘。

重標(biāo)記(relabel L1 to L2)
重標(biāo)記
刪除節(jié)點(diǎn)(delete node L2)
刪除節(jié)點(diǎn)
插入節(jié)點(diǎn)(insert L2 to L1)
插入節(jié)點(diǎn)

1.2 標(biāo)準(zhǔn)的Diff算法

標(biāo)準(zhǔn)的Tree-Diff算法饺谬,使用了嚴(yán)謹(jǐn)?shù)拇鷥r(jià)函數(shù)(Cost Function),適用于很多需要樹(shù)結(jié)構(gòu)對(duì)比的場(chǎng)景谣拣,如:計(jì)算生物學(xué)募寨、圖像分析、結(jié)構(gòu)化文本數(shù)據(jù)庫(kù)等森缠。然而其算法的時(shí)間復(fù)雜度為O(n^3)拔鹰,在瀏覽器環(huán)境中,要達(dá)到每次更新都可以整體刷新界面的目的贵涵,這樣高的算法復(fù)雜度會(huì)有性能問(wèn)題列肢。

2 React Diff算法(reconciliation algorithm)

React開(kāi)發(fā)團(tuán)隊(duì)結(jié)合Web界面的特點(diǎn)對(duì)標(biāo)準(zhǔn)的Diff算法做出了優(yōu)化恰画,使得其時(shí)間復(fù)雜度降低為O(n)。這一算法例书,即reconciliation algorithm锣尉,常見(jiàn)中文翻譯為“協(xié)調(diào)算法”,個(gè)人更傾向于“權(quán)衡算法”這一更形象的翻譯决采,因?yàn)樵撍惴ㄋ鶎?shí)現(xiàn)的就是權(quán)衡利弊之后的簡(jiǎn)化自沧。這種算法的基本假設(shè)如下:

  • Web UI中DOM節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作較少,可以忽略树瞭。
  • 兩個(gè)相同組件產(chǎn)生類似的DOM結(jié)構(gòu)拇厢,不同的組件產(chǎn)生不同的DOM結(jié)構(gòu)。
  • 對(duì)于同一層次的一組相同類型的子節(jié)點(diǎn)晒喷,它們可以通過(guò)唯一的key進(jìn)行區(qū)分孝偎。

2.1 單一節(jié)點(diǎn)對(duì)比

為了對(duì)比兩個(gè)樹(shù)結(jié)構(gòu),需要先能夠比較兩個(gè)節(jié)點(diǎn)凉敲,在React中就是Virtual DOM樹(shù)(以下簡(jiǎn)稱“VD樹(shù)”)的節(jié)點(diǎn)衣盾,而VD樹(shù)節(jié)點(diǎn)的不同又分兩種情況:

  • 節(jié)點(diǎn)類型不同
  • 節(jié)點(diǎn)類型相同但屬性(props)不同

2.1.1 節(jié)點(diǎn)類型不同

當(dāng)VD樹(shù)中同一節(jié)點(diǎn)位置前后節(jié)點(diǎn)類型不同時(shí),React直接刪除舊的節(jié)點(diǎn)爷抓,然后創(chuàng)建并插入新的節(jié)點(diǎn)势决,使用的節(jié)點(diǎn)基本操作方法為刪除和插入。

renderA: <OldNode />
renderB: <NewNode />
=> [removeNode <OldNode />], [insertNode <NewNode />]

在React組建的生命周期方法中蓝撇,老節(jié)點(diǎn)OldNode的componentWillUnmount()方法最先觸發(fā)果复,之后新節(jié)點(diǎn)NewNode的componentWillMount()和componentDidMount()方法依次觸發(fā)。

2.1.2 節(jié)點(diǎn)類型相同但屬性不同

節(jié)點(diǎn)類型相同但屬性不同的情況下渤昌,React會(huì)對(duì)VD數(shù)節(jié)點(diǎn)的屬性進(jìn)行重設(shè)虽抄,從而實(shí)現(xiàn)節(jié)點(diǎn)的轉(zhuǎn)換,使用的節(jié)點(diǎn)基本操作方法為重標(biāo)記独柑。因?yàn)镽eact組件的實(shí)例保持不變迈窟,該組件的state也會(huì)保持不變,組件的componentWillReceiveProps()忌栅、componentWillUpdate()菠隆、render()方法依次觸發(fā)。

renderA: <Node className="old" />
renderB: <Node className="new" />
=> [replaceAttribute className "new"]

VD樹(shù)節(jié)點(diǎn)的style屬性稍有不同狂秘,因?yàn)閭魅氲氖菍?duì)象而不是字符串,React會(huì)只更新改變了的樣式屬性躯肌。

如果開(kāi)發(fā)者能夠明確節(jié)點(diǎn)是否有變化者春,則可以通過(guò)shouldComponentUpdate(nextProp, nextState)方法來(lái)決定是否進(jìn)行Diff運(yùn)算。

2.2 逐層進(jìn)行節(jié)點(diǎn)對(duì)比(分層求異)

React只會(huì)對(duì)同一層級(jí)(下圖中相同顏色方框內(nèi))的DOM節(jié)點(diǎn)進(jìn)行比較清女,即同一個(gè)父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)钱烟。如果發(fā)現(xiàn)已經(jīng)不存在的節(jié)點(diǎn),則該節(jié)點(diǎn)及其子節(jié)點(diǎn)會(huì)被完全刪除掉,不再作進(jìn)一步的比較拴袭。這樣只需要對(duì)樹(shù)進(jìn)行一次遍歷读第,便能完成整個(gè)DOM樹(shù)的比較。這一方法正是基于前一假設(shè)拥刻,即不同類型的組件產(chǎn)生不同的VD樹(shù)結(jié)構(gòu)怜瞒。


逐層進(jìn)行節(jié)點(diǎn)對(duì)比

例如,在下圖的VD樹(shù)結(jié)構(gòu)轉(zhuǎn)換過(guò)程中般哼,看似應(yīng)該是把整個(gè)A節(jié)點(diǎn)從根節(jié)點(diǎn)插入到D節(jié)點(diǎn)中吴汪,最直觀的操作方式應(yīng)該是:

Root.remove(A).find(D).append(A)

然而,React的“權(quán)衡算法”只會(huì)簡(jiǎn)單對(duì)比同一層級(jí)的節(jié)點(diǎn)蒸眠,不同層級(jí)的節(jié)點(diǎn)只有刪除和插入漾橙。所以,React實(shí)際的操作方式卻是直接刪除節(jié)點(diǎn)A楞卡,至于A的子節(jié)點(diǎn)B和C則直接被忽略了霜运;而當(dāng)發(fā)現(xiàn)D節(jié)點(diǎn)多了一個(gè)子節(jié)點(diǎn)A時(shí),則會(huì)插入一個(gè)新的節(jié)點(diǎn)A作為自己點(diǎn)蒋腮。過(guò)程如下:

A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);

這樣的操作看似是把簡(jiǎn)單的問(wèn)題變得復(fù)雜了淘捡,在這一例子中的確如此,畢竟根據(jù)NFL定理(no free lunch theorem)徽惋,任何優(yōu)化都是有代價(jià)的案淋,Reconciliation algorithm也不例外。之所以稱之為“權(quán)衡算法”险绘,也正是因?yàn)檫@一方法以忽略不同類型節(jié)點(diǎn)的子節(jié)點(diǎn)的對(duì)比為代價(jià)踢京,進(jìn)而大大降低了Diff算法的時(shí)間復(fù)雜度。

鑒于這一情況宦棺,為提高性能瓣距,應(yīng)盡量避免進(jìn)行DOM節(jié)點(diǎn)跨層級(jí)的操作。此外代咸,保持穩(wěn)定的DOM結(jié)構(gòu)也有助于性能提升蹈丸,可以通過(guò)類名和CSS來(lái)控制節(jié)點(diǎn)的顯示隱藏。

2.3 列表節(jié)點(diǎn)的對(duì)比

2.1和2.2中分別介紹了React diff算法對(duì)單一節(jié)點(diǎn)呐芥,和不在同一層中的節(jié)點(diǎn)的對(duì)比方式逻杖。與前兩者不同的是,一組列表節(jié)點(diǎn)往往有相同的類型和屬性思瘟,此外荸百,列表節(jié)點(diǎn)的常見(jiàn)操作包含插入、刪除和排序滨攻。如果每個(gè)列表節(jié)點(diǎn)沒(méi)有唯一標(biāo)識(shí)够话,則React無(wú)法識(shí)別每一個(gè)節(jié)點(diǎn)蓝翰,那么只能更新所有列表節(jié)點(diǎn)造成效率低下。

例如女嘲,要在下圖的列表節(jié)點(diǎn)B和C中間插入節(jié)點(diǎn)F畜份,如果React無(wú)法識(shí)別每一個(gè)節(jié)點(diǎn),則只能依次更新所有節(jié)點(diǎn)欣尼,如果節(jié)點(diǎn)數(shù)量很大爆雹,會(huì)有明顯的性能損耗。



這時(shí)媒至,“key”這個(gè)屬性就要登場(chǎng)了顶别,React通過(guò)key這個(gè)屬性發(fā)現(xiàn)更新前后相同的列表節(jié)點(diǎn),因此無(wú)需進(jìn)行相同節(jié)點(diǎn)刪除和創(chuàng)建拒啰,只需要將舊的節(jié)點(diǎn)的位置進(jìn)行移動(dòng)驯绎。如果有舊的節(jié)點(diǎn)被刪除或新的節(jié)點(diǎn)被插入,也不會(huì)造成其他節(jié)點(diǎn)被重新創(chuàng)建谋旦。
再上圖的例子中剩失,React會(huì)通過(guò)每個(gè)節(jié)點(diǎn)的唯一標(biāo)識(shí)(key),準(zhǔn)確找到插入新節(jié)點(diǎn)F的位置册着,同時(shí)保留其他節(jié)點(diǎn)拴孤。



如果列表節(jié)點(diǎn)能夠獲取到唯一的id屬性,那么選擇id作為key最好不過(guò)甲捏,如果沒(méi)有id或較難獲取演熟,則退而求其次選用.map(item, index)方法中的索引index作為key。

3 總結(jié)

  • React Diff 通過(guò)“權(quán)衡算法”司顿,把復(fù)雜度O(n^3)的問(wèn)題轉(zhuǎn)化為復(fù)雜度O(n)的問(wèn)題芒粹;
  • 通過(guò)”分層求異“策略進(jìn)行優(yōu)化,假設(shè)不同類型的組件產(chǎn)生不同的DOM樹(shù)結(jié)構(gòu)大溜,主動(dòng)放棄了對(duì)不同父節(jié)點(diǎn)的子節(jié)點(diǎn)的對(duì)比化漆;
  • 開(kāi)發(fā)組建時(shí),保持穩(wěn)定的DOM結(jié)構(gòu)钦奋、避免跨層級(jí)移動(dòng)DOM節(jié)點(diǎn)座云,有助于性能提升;
  • 通過(guò)設(shè)置唯一key的策略付材,對(duì)列表節(jié)點(diǎn)進(jìn)行了diff算法優(yōu)化朦拖。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市厌衔,隨后出現(xiàn)的幾起案子贞谓,更是在濱河造成了極大的恐慌,老刑警劉巖葵诈,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裸弦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡作喘,警方通過(guò)查閱死者的電腦和手機(jī)理疙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泞坦,“玉大人窖贤,你說(shuō)我怎么就攤上這事》∷” “怎么了赃梧?”我有些...
    開(kāi)封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)豌熄。 經(jīng)常有香客問(wèn)我授嘀,道長(zhǎng),這世上最難降的妖魔是什么锣险? 我笑而不...
    開(kāi)封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任蹄皱,我火速辦了婚禮,結(jié)果婚禮上芯肤,老公的妹妹穿的比我還像新娘粪小。我一直安慰自己静檬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著绳瘟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪毡琉。 梳的紋絲不亂的頭發(fā)上蕾久,一...
    開(kāi)封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音际邻,去河邊找鬼芯丧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛世曾,可吹牛的內(nèi)容都是我干的缨恒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼轮听,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼骗露!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起血巍,我...
    開(kāi)封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萧锉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后述寡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體柿隙,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叶洞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了禀崖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衩辟。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖波附,靈堂內(nèi)的尸體忽然破棺而出艺晴,到底是詐尸還是另有隱情,我是刑警寧澤掸屡,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布封寞,位于F島的核電站,受9級(jí)特大地震影響仅财,放射性物質(zhì)發(fā)生泄漏狈究。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一满着、第九天 我趴在偏房一處隱蔽的房頂上張望谦炒。 院中可真熱鬧,春花似錦风喇、人聲如沸宁改。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)还蹲。三九已至,卻和暖如春耙考,著一層夾襖步出監(jiān)牢的瞬間谜喊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工倦始, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斗遏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓鞋邑,卻偏偏與公主長(zhǎng)得像诵次,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枚碗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容