在 React17+ 中 DOM-DIFF 就是根據(jù)老的 fiber 樹和最新的 JSX 對比生成新的 fiber 樹的過程
正常樹的 diff 算法時(shí)間復(fù)雜度是 O(n^3),但是這個(gè)時(shí)間復(fù)雜度太高,所以 React 進(jìn)行了一些優(yōu)化
React 中的優(yōu)化規(guī)則
- 只對同級節(jié)點(diǎn)進(jìn)行對比,如果 DOM 節(jié)點(diǎn)跨層級移動(dòng),則 React 不會復(fù)用
- 不同類型的元素會產(chǎn)出不同的結(jié)構(gòu),會銷毀老的結(jié)構(gòu),創(chuàng)建新的結(jié)構(gòu)
- 可以通過 key 標(biāo)識移動(dòng)的元素

單節(jié)點(diǎn),新的元素只有一個(gè)的情況
1. type 不同
<div>
// 在調(diào)和階段,需要把這個(gè)老節(jié)點(diǎn)標(biāo)記為刪除
<h1 key="null">h1</h1>
</div>
更新為:
<div>
// 生成新的 fiber 節(jié)點(diǎn)并標(biāo)記為插入
<h2 key="null">h2<h2>
</div>
在 commit 階段,會執(zhí)行兩個(gè)操作:
div.removeChild(h1)div.appendChild(h2)
2. type 和 key 都不同
<div>
// h1 標(biāo)記為刪除
<h1 key="h1">h1</h1>
<h2 key="h2">h2</h2>
<h3 key="h3">h3</h2>
</div>
更新為:
<div>
<h2 key="h2">h2</h2>
</div>
對于這個(gè)案例,先拿 h2 和 h1 匹配,發(fā)現(xiàn)不能復(fù)用,把 h1 標(biāo)記為刪除,匹配到 h2,可以復(fù)用,然后把剩下的節(jié)點(diǎn)全部刪除
div.removeChild(h1)div.removeChild(h3)
3. key 相同 type 也相同
<div>
<h1 key="h1">h1</h1>
</div>
更新為:
<div>
<h1 key="h1">h1 - new</h1>
</div>
如果對比后發(fā)現(xiàn)新老節(jié)點(diǎn)一樣的,那么會復(fù)用老節(jié)點(diǎn),復(fù)用老節(jié)點(diǎn)的 DOM 元素和 Fiber 對象
再看屬性有無變更,如果有變化,則會把此 Fiber 節(jié)點(diǎn)標(biāo)記為更新
h1.innerHTML = 'h1 - new'
4. key 相同但是 type 不同,直接刪除所有老節(jié)點(diǎn)
<div>
<h1 key="h1">h1</h1>
<h2 key="h2">h2</h2>
</div>
更新為:
<div>
<p key="h1">p</p>
</div>
如果 key 相同,但是 type 不同,則不再進(jìn)行后續(xù)對比了,因?yàn)?key 一樣代表是同一個(gè)元素,如果 type 不一樣說明節(jié)點(diǎn)已經(jīng)發(fā)生改變,直接把老節(jié)點(diǎn)全部刪掉,插入新節(jié)點(diǎn)即可
key 相同表示這是同一個(gè)元素
div.removeChild(h1)div.removeChild(h2)div.appendChild(p)
多節(jié)點(diǎn)
如果有多個(gè)節(jié)點(diǎn),節(jié)點(diǎn)有可能會更新,刪除,新增,多節(jié)點(diǎn)的時(shí)候會經(jīng)過兩輪遍歷
第一輪遍歷主要處理節(jié)點(diǎn)的更新,更新包括屬性和類型的更新
第二輪遍歷主要處理節(jié)點(diǎn)的新增、刪除和移動(dòng)
移動(dòng)時(shí)的原則是盡量少量的移動(dòng),如果必須有一個(gè)要?jiǎng)樱匚桓叩牟粍?dòng),地位低的動(dòng)
對比,都可復(fù)用,只需更新
<ul>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
更新為:
<ul>
<li key="A">A - new</li>
<li key="B">B - new</li>
<li key="C">C - new</li>
<li key="D">D - new</li>
</ul>
最后會得到一個(gè)操作序列:
- 更新 A
- 更新 B
- 更新 C
- 更新 D
key 相同,type 不同
<ul>
<li key="A">A</li> // Fiber 節(jié)點(diǎn)
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
更新為:
<ul>
<div key="A">A - new</div> // JSX 節(jié)點(diǎn)
<li key="B">B - new</li>
<li key="C">C - new</li>
<li key="D">D - new</li>
</ul>
第一步比較第一個(gè)節(jié)點(diǎn),發(fā)現(xiàn)不能復(fù)用,老的標(biāo)記刪除,新的標(biāo)記插入
后面的都可復(fù)用
最后結(jié)論是:
刪除老的 li A,插入 divA,更新 B C D

DOM-DIFF 是一個(gè)對比老的 Fiber 鏈表和新的 JSX 數(shù)組,生成新的 Fiber 鏈表的過程
老的是 Fiber,新的是 JSX
key 不同退出第一輪循環(huán)
因?yàn)槿绻?key 不一樣已經(jīng)發(fā)生位置變化了
<ul>
<li key="A" style={oldStyle}>A</li>
<li key="B" style={oldStyle}>B</li>
<li key="C" style={oldStyle}>C</li>
<li key="D" style={oldStyle}>D</li>
<li key="E" style={oldStyle}>E</li>
<li key="F" style={oldStyle}>F</li>
</ul>
更新為:
<ul>
<li key="A" style={oldStyle}>A - new</li>
<li key="C" style={oldStyle}>C - new</li>
<li key="E" style={oldStyle}>E - new</li>
<li key="B" style={oldStyle}>B - new</li>
<li key="G" style={oldStyle}>G</li>
</ul>

首先第一輪循環(huán)
oldA 和 newA 比較,一樣,可以復(fù)用,更新 A
oldB 和 newC 比較,key 不一樣,跳出第一輪循環(huán)
進(jìn)行第二輪循環(huán)
建一個(gè) map 對象 let map = {'B': B, 'C': C, ' 'D': D, 'E': E, 'F': F}(不處理 A,因?yàn)?A 在第一輪循環(huán)已經(jīng)處理完了) 表示還沒有復(fù)用的節(jié)點(diǎn)
繼續(xù)遍歷新節(jié)點(diǎn),到 newC 了
newC 節(jié)點(diǎn)在 map 找有沒有 key 為 C 的 fiber 節(jié)點(diǎn)
如果有,并且可以復(fù)用(Fiber 和 DOM 可以復(fù)用),說明只是位置變了,把 oldC 標(biāo)標(biāo)記為更新
lastPlacedIndex表示最大的不需要?jiǎng)拥睦系墓?jié)點(diǎn)的索引oldC 的 oldIndex 是 2,oldIndex > lastPlacedIndex,所以 C 不需要?jiǎng)?/p>
lastPlacedIndex = 2
map 中刪除 C
再看 newE
在 map 里面找 E,找到了 oldE,oldIndex 是 4,lastPlacedIndex < 4,E 也不動(dòng)
lastPlacedIndex = 4
map 中刪除 E
到 newB 了,找到 oldB,oldIndex = 1 < lastPlacedIndex,oldB 需要往后挪
newG 不在 map 里面,標(biāo)記為插入
等 JSX 數(shù)組全部遍歷完之后,把 map 里面全部 fiber 節(jié)點(diǎn)標(biāo)記為刪除,也就是 oldE 和 oldF
在 react 里面,每一個(gè)操作都有一個(gè)權(quán)重(上圖中那個(gè)表),每種操作都有權(quán)重,這些都記錄在 effectList 里面
每次 DOM Diff 會生成一個(gè) effectList,在 commitMutationEffects(ReactFiberWorkLoop.old.js) 會使用,根據(jù) effectList 進(jìn)行操作
另外,那個(gè)優(yōu)先級在 ReactFiberFlags.js 里面可以找到