React 虛擬DOM Diff演算法解析

2021-08-14 08:12:39 字數 3109 閱讀 1329

react中最神奇的部分莫過於虛擬dom,以及其高效的diff演算法。這讓我們可以無需擔心效能問題而」毫無顧忌」的隨時「重新整理」整個頁面,由虛擬dom來確保只對介面上真正變化的部分進行實際的dom操作。react在這一部分已經做到足夠透明,在實際開發中我們基本無需關心虛擬dom是如何運作的。然而,理解其執行機制不僅有助於更好的理解react元件的生命週期,而且對於進一步優化react程式也會有很大幫助。

有些小夥伴可能覺得:react 無非就是引入 diff 這一概念,且 diff 演算法也並非其首創,何必吹噓的如此天花亂墜呢?

其實,正是因為 diff 演算法的普識度高,就更應該認可 react 針對 diff 演算法優化所做的努力與貢獻,更能體現 react 開發者們的魅力與智慧型!

傳統 diff 演算法

計算一棵樹形結構轉換成另一棵樹形結構的最少操作,是乙個複雜且值得研究的問題。傳統 diff 演算法通過迴圈遞迴對節點進行依次對比,效率低下,演算法複雜度達到 o(n^3),其中 n 是樹中節點的總數。o(n^3) 到底有多可怕,這意味著如果要展示1000個節點,就要依次執行上十億次的比較。這種指數型的效能消耗對於前端渲染場景來說代價太高了!現今的 cpu 每秒鐘能執行大約30億條指令,即便是最高效的實現,也不可能在一秒內計算出差異情況。

如果 react 只是單純的引入 diff 演算法而沒有任何的優化改進,那麼其效率是遠遠無法滿足前端渲染所要求的效能。

因此,想要將 diff 思想引入 virtual dom,就需要設計一種穩定高效的 diff 演算法,而 react 做到了!

那麼,react diff 到底是如何實現的呢?

react diff

傳統 diff 演算法的複雜度為 o(n^3),顯然這是無法滿足效能要求的。react 通過制定大膽的策略,將 o(n^3) 複雜度的問題轉換成 o(n) 複雜度的問題。

diff 策略

web ui 中 dom 節點跨層級的移動操作特別少,可以忽略不計。

擁有相同類的兩個元件將會生成相似的樹形結構,擁有不同類的兩個元件將會生成不同的樹形結構。

對於同一層級的一組子節點,它們可以通過唯一 id 進行區分。

不同節點型別的比較

為了在樹之間進行比較,我們首先要能夠比較兩個節點,在react中即比較兩個虛擬dom節點,當兩個節點不同時,應該如何處理。這分為兩種情況:(1)節點型別不同 ,(2)節點型別相同,但是屬性不同。本節先看第一種情況。

當在樹中的同一位置前後輸出了不同型別的節點,react直接刪除前面的節點,然後建立並插入新的節點。假設我們在樹的同一位置前後兩次輸出不同型別的節點。

rendera: 

renderb:

=> [removenode

], [insertnode ]

當乙個節點從div變成span時,簡單的直接刪除div節點,並插入乙個新的span節點。這符合我們對真實dom操作的理解。

需要注意的是,刪除節點意味著徹底銷毀該節點,而不是再後續的比較中再去看是否有另外乙個節點等同於該刪除的節點。如果該刪除的節點之下有子節點,那麼這些子節點也會被完全刪除,它們也不會用於後面的比較。這也是演算法複雜能夠降低到o(n)的原因。

上面提到的是對虛擬dom節點的操作,而同樣的邏輯也被用在react元件的比較,例如:

rendera: 

renderb:

=> [removenode ], [insertnode ]

當react在同乙個位置遇到不同的元件時,也是簡單的銷毀第乙個元件,而把新建立的元件加上去。這正是應用了第乙個假設,不同的元件一般會產生不一樣的dom結構,與其浪費時間去比較它們基本上不會等價的dom結構,還不如完全建立乙個新的元件加上去。

由這一react對不同型別的節點的處理邏輯我們很容易得到推論,那就是react的dom diff演算法實際上只會對樹進行逐層比較,如下所述。

逐層進行節點比較

提到樹,相信大多數同學立刻想到的是二叉樹,遍歷,最短路徑等複雜的資料結構演算法。而在react中,樹的演算法其實非常簡單,那就是兩棵樹只會對同一層次的節點進行比較。如下圖所示:

react只會對相同顏色方框內的dom節點進行比較,即同乙個父節點下的所有子節點。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個dom樹的比較。

例如,考慮有下面的dom結構轉換:

a節點被整個移動到d節點下,直觀的考慮dom diff操作應該是

a.parent.remove(a);
但因為react只會簡單的考慮同層節點的位置變換,對於不同層的節點,只有簡單的建立和刪除。當根節點發現子節點中a不見了,就會直接銷毀a;而當d發現自己多了乙個子節點a,則會建立乙個新的a作為子節點。因此對於這種結構的轉變的實際操作是:

a.destroy();

a = new a();

可以看到,以a為根節點的樹被整個重新建立。

雖然看上去這樣的演算法有些「簡陋」,但是其基於的是第乙個假設:兩個不同元件一般產生不一樣的dom結構。根據react官方部落格,這一假設至今為止沒有導致嚴重的效能問題。這當然也給我們乙個提示,在實現自己的元件時,保持穩定的dom結構會有助於效能的提公升。例如,我們有時可以通過css隱藏或顯示某些節點,而不是真的移除或新增dom節點。

總結1、react 通過制定大膽的 diff 策略,將 o(n3) 複雜度的問題轉換成 o(n) 複雜度的問題;

2、react 通過分層求異的策略,對 tree diff 進行演算法優化;

3、react 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行演算法優化;

4、react 通過設定唯一 key的策略,對 element diff 進行演算法優化;

建議:在開發元件時,保持穩定的 dom 結構會有助於效能的提公升;

建議:在開發過程中,儘量減少類似將最後乙個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 react 的渲染效能。

虛擬dom diff演算法

通過render函式解析jsx,將其轉換成 vdom結構var vdom content 初次渲染 vdom vdom渲染成 真實dom render函式 資料更改了data.name 駿哥 vdom content 使用diff演算法比對兩次vdom,生成patch物件diff演算法是比較兩個檔案...

虛擬dom diff演算法

虛擬dom是什麼?需求 有乙個變數 count 的初始值時 0,經過一系列運算,得到10001,然後將結果寫入box中 var box document.queryselector box 我們可能會這麼寫 var count 0 console.time a for var i 0 i 10001...

虛擬Dom diff演算法

1.虛擬dom樹的遍歷 2.parent節點下的children的比較 3.diff完成之後對真實3.dom的操作時機 虛擬dom的遍歷 虛擬dom說到底只是一顆樹形結構,對於樹的遍歷我們知道有深度遍歷和廣度遍歷 一 指代不同 1 深度優先遍歷 是對每乙個可能的分支路徑深入到不能再深入為止,而且每個...