1. 程式人生 > >react diff演算法淺析

react diff演算法淺析

diff演算法作為Virtual DOM的加速器,其演算法的改進優化是React整個介面渲染的基礎和效能的保障,同時也是React原始碼中最神祕的,最不可思議的部分

1.傳統diff演算法
計算一棵樹形結構轉換為另一棵樹形結構需要最少步驟,如果使用傳統的diff演算法通過迴圈遞迴遍歷節點進行對比,其複雜度要達到O(n^3),其中n是節點總數,效率十分低下,假設我們要展示1000個節點,那麼我們就要依次執行上十億次的比較。

下面附上一則簡單的傳統diff演算法:


let result = [];
// 比較葉子節點
const diffLeafs = function (beforeLeaf, afterLeaf) {
// 獲取較大節點樹的長度
let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length);
// 迴圈遍歷
for (let i = 0; i < count; i++) {
const beforeTag = beforeLeaf.children[i];
const afterTag = afterLeaf.children[i];
// 新增 afterTag 節點
if (beforeTag === undefined) {
result.push({ type: "add", element: afterTag });
// 刪除 beforeTag 節點
} else if (afterTag === undefined) {
result.push({ type: "remove", element: beforeTag });
// 節點名改變時,刪除 beforeTag 節點,新增 afterTag 節點
} else if (beforeTag.tagName !== afterTag.tagName) {
result.push({ type: "remove", element: beforeTag });
result.push({ type: "add", element: afterTag });
// 節點不變而內容改變時,改變節點
} else if (beforeTag.innerHTML !== afterTag.innerHTML) {
if (beforeTag.children.length === 0) {
result.push({
type: "changed",
beforeElement: beforeTag,
afterElement: afterTag,
html: afterTag.innerHTML
});
} else {
// 遞迴比較
diffLeafs(beforeTag, afterTag);
}
}
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2.react diff演算法
1. diff策略
下面介紹一下react diff演算法的3個策略

Web UI 中DOM節點跨層級的移動操作特別少,可以忽略不計
擁有相同類的兩個元件將會生成相似的樹形結構,擁有不同類的兩個元件將會生成不同的樹形結構。
對於同一層級的一組子節點,它們可以通過唯一id進行區分。
對於以上三個策略,react分別對tree diff,component diff,element diff進行演算法優化。

2.tree diff
基於策略一,WebUI中DOM節點跨層級的移動操作少的可以忽略不計,React對Virtual DOM樹進行層級控制,只會對相同層級的DOM節點進行比較,即同一個父元素下的所有子節點,當發現節點已經不存在了,則會刪除掉該節點下所有的子節點,不會再進行比較。這樣只需要對DOM樹進行一次遍歷,就可以完成整個樹的比較。複雜度變為O(n);

疑問:當我們的DOM節點進行跨層級操作時,diff會有怎麼樣的表現呢?

如下圖所示,A節點及其子節點被整個移動到D節點下面去,由於React只會簡單的考慮同級節點的位置變換,而對於不同層級的節點,只有建立和刪除操作,所以當根節點發現A節點消失了,就會刪除A節點及其子節點,當D發現多了一個子節點A,就會建立新的A作為其子節點。
此時,diff的執行情況是:

createA-->createB-->createC-->deleteA
1

由此可以發現,當出現節點跨層級移動時,並不會出現想象中的移動操作,而是會進行刪除,重新建立的動作,這是一種很影響React效能的操作。因此官方也不建議進行DOM節點跨層級的操作。

3.componnet diff
React是基於元件構建應用的,對於元件間的比較所採用的策略也是非常簡潔和高效的。

如果是同一個型別的元件,則按照原策略進行Virtual DOM比較。
如果不是同一型別的元件,則將其判斷為dirty component,從而替換整個組價下的所有子節點。
如果是同一個型別的元件,有可能經過一輪Virtual DOM比較下來,並沒有發生變化。如果我們能夠提前確切知道這一點,那麼就可以省下大量的diff運算時間。因此,React允許使用者通過shouldComponentUpdate()來判斷該元件是否需要進行diff演算法分析。
如下圖所示,當元件D變為元件G時,即使這兩個元件結構相似,一旦React判斷D和G是不用型別的元件,就不會比較兩者的結構,而是直接刪除元件D,重新建立元件G及其子節點。雖然當兩個元件是不同型別但結構相似時,進行diff演算法分析會影響效能,但是畢竟不同型別的元件存在相似DOM樹的情況在實際開發過程中很少出現,因此這種極端因素很難在實際開發過程中造成重大影響。


4.element diff
當節點屬於同一層級時,diff提供了3種節點操作,分別為INSERT_MARKUP(插入),MOVE_EXISTING(移動),REMOVE_NODE(刪除)。

INSERT_MARKUP:新的元件型別不在舊集合中,即全新的節點,需要對新節點進行插入操作。
MOVE_EXISTING:舊集合中有新元件型別,且element是可更新的型別,這時候就需要做移動操作,可以複用以前的DOM節點。
REMOVE_NODE:舊元件型別,在新集合裡也有,但對應的element不同則不能直接複用和更新,需要執行刪除操作,或者舊元件不在新集合裡的,也需要執行刪除操作。