人人都能讀懂的react源碼解析(大廠高薪必備)
8.diff算法(媽媽再也不擔心我的diff面試了)
視頻課程&調(diào)試demos
視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler沛豌、reconciler发魄、renderer泰偿、fiber等,并且詳細debug源碼和分析铸豁,過程更清晰灌曙。
視頻課程:進入課程
demos:demo
課程結(jié)構(gòu):
- 開篇(聽說你還在艱難的啃react源碼)
- react心智模型(來來來,讓大腦有react思維吧)
- Fiber(我是在內(nèi)存中的dom)
- 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
- state更新流程(setState里到底發(fā)生了什么)
- render階段(厲害了,我有創(chuàng)建Fiber的技能)
- commit階段(聽說renderer幫我們打好標記了,映射真實節(jié)點吧)
- diff算法(媽媽再也不擔心我的diff面試了)
- hooks源碼(想知道Function Component是怎樣保存狀態(tài)的嘛)
- scheduler&lane模型(來看看任務(wù)是暫停、繼續(xù)和插隊的)
- concurrent mode(并發(fā)模式是什么樣的)
- 手寫迷你react(短小精悍就是我)
在render階段更新Fiber節(jié)點時节芥,我們會調(diào)用reconcileChildFibers對比current Fiber和jsx對象構(gòu)建workInProgress Fiber在刺,這里current Fiber是指當前dom對應(yīng)的fiber樹,jsx是class組件render方法或者函數(shù)組件的返回值头镊。
在reconcileChildFibers中會根據(jù)newChild的類型來進入單節(jié)點的diff或者多節(jié)點diff
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
): Fiber | null {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
//單一節(jié)點diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
}
}
//...
if (isArray(newChild)) {
//多節(jié)點diff
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 刪除節(jié)點
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
diff過程的主要流程如下圖:
我們知道對比兩顆樹的復雜度本身是O(n3)蚣驼,對我們的應(yīng)用來說這個是不能承受的量級,react為了降低復雜度相艇,提出了三個前提:
只對同級比較颖杏,跨層級的dom不會進行復用
不同類型節(jié)點生成的dom樹不同,此時會直接銷毀老節(jié)點及子孫節(jié)點坛芽,并新建節(jié)點
-
可以通過key來對元素diff的過程提供復用的線索留储,例如:
const a = ( <> <p key="0">0</p> <p key="1">1</p> </> ); const b = ( <> <p key="1">1</p> <p key="0">0</p> </> );
如果a和b里的元素都沒有key,因為節(jié)點的更新前后文本節(jié)點不同咙轩,導致他們都不能復用获讳,所以會銷毀之前的節(jié)點,并新建節(jié)點活喊,但是現(xiàn)在有key了丐膝,b中的節(jié)點會在老的a中尋找key相同的節(jié)點嘗試復用,最后發(fā)現(xiàn)只是交換位置就可以完成更新钾菊,具體對比過程后面會講到帅矗。
單節(jié)點diff
單點diff有如下幾種情況:
- key和type相同表示可以復用節(jié)點
- key不同直接標記刪除節(jié)點,然后新建節(jié)點
- key相同type不同结缚,標記刪除該節(jié)點和兄弟節(jié)點损晤,然后新創(chuàng)建節(jié)點
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber { const key = element.key; let child = currentFirstChild; //child節(jié)點不為null執(zhí)行對比 while (child !== null) { // 1.比較key if (child.key === key) { // 2.比較type switch (child.tag) { //... default: { if (child.elementType === element.type) { // type相同則可以復用 返回復用的節(jié)點 return existing; } // type不同跳出 break; } } //key相同,type不同則把fiber及和兄弟fiber標記刪除 deleteRemainingChildren(returnFiber, child); break; } else { //key不同直接標記刪除該節(jié)點 deleteChild(returnFiber, child); } child = child.sibling; } //新建新Fiber }
多節(jié)點diff
多節(jié)點diff比較復雜红竭,我們分三種情況進行討論尤勋,其中a表示更新前的節(jié)點,b表示更新后的節(jié)點
-
屬性變化
const a = ( <> <p key="0" name='0'>0</p> <p key="1">1</p> </> ); const b = ( <> <p key="0" name='00'>0</p> <p key="1">1</p> </> );
-
type變化
const a = ( <> <p key="0">0</p> <p key="1">1</p> </> ); const b = ( <> <div key="0">0</div> <p key="1">1</p> </> );
-
新增節(jié)點
const a = ( <> <p key="0">0</p> <p key="1">1</p> </> ); const b = ( <> <p key="0">0</p> <p key="1">1</p> <p key="2">2</p> </> );
-
節(jié)點刪除
const a = ( <> <p key="0">0</p> <p key="1">1</p> <p key="2">2</p> </> ); const b = ( <> <p key="0">0</p> <p key="1">1</p> </> );
-
節(jié)點位置變化
const a = ( <> <p key="0">0</p> <p key="1">1</p> </> ); const b = ( <> <p key="1">1</p> <p key="0">0</p> </> );
在源碼中多節(jié)點diff會經(jīng)歷三次遍歷茵宪,第一次遍歷處理節(jié)點的更新(包括props更新和type更新和刪除)最冰,第二次遍歷處理其他的情況(節(jié)點新增),其原因在于在大多數(shù)的應(yīng)用中稀火,節(jié)點更新的頻率更加頻繁暖哨,第三次處理位節(jié)點置改變
-
第一次遍歷
因為老的節(jié)點存在于current Fiber中,所以它是個鏈表結(jié)構(gòu)凰狞,還記得Fiber雙緩存結(jié)構(gòu)嘛篇裁,節(jié)點通過child沛慢、return、sibling連接达布,而newChildren存在于jsx當中团甲,所以遍歷對比的時候,首先讓newChildren[i]
與
oldFiber對比黍聂,然后讓i++躺苦、nextOldFiber = oldFiber.sibling。在第一輪遍歷中产还,會處理三種情況匹厘,其中第1,2兩種情況會結(jié)束第一次循環(huán)- key不同脐区,第一次循環(huán)結(jié)束
- newChildren或者oldFiber遍歷完愈诚,第一次循環(huán)結(jié)束
- key同type不同,標記oldFiber為DELETION
- key相同type相同則可以復用
newChildren遍歷完坡椒,oldFiber沒遍歷完扰路,在第一次遍歷完成之后將oldFiber中沒遍歷完的節(jié)點標記為DELETION,即刪除的DELETION Tag
-
第二次遍歷
第二次遍歷考慮三種情況
1\. newChildren和oldFiber都遍歷完:多節(jié)點diff過程結(jié)束 2\. newChildren沒遍歷完倔叼,oldFiber遍歷完汗唱,將剩下的newChildren的節(jié)點標記為Placement,即插入的Tag
- newChildren和oldFiber沒遍歷完丈攒,則進入節(jié)點移動的邏輯
-
第三次遍歷
主要邏輯在placeChild函數(shù)中哩罪,例如更新前節(jié)點順序是ABCD,更新后是ACDB
newChild中第一個位置的A和oldFiber第一個位置的A巡验,key相同可復用际插,lastPlacedIndex=0
newChild中第二個位置的C和oldFiber第二個位置的B,key不同跳出第一次循環(huán)显设,將oldFiber中的BCD保存在map中
newChild中第二個位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移動框弛,lastPlacedIndex=2
-
newChild中第三個位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移動,lastPlacedIndex=3
-
newChild中第四個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最后
看圖更直觀
_15 -
例如更新前節(jié)點順序是ABCD捕捂,更新后是DABC
newChild中第一個位置的D和oldFiber第一個位置的A瑟枫,key不相同不可復用,將oldFiber中的ABCD保存在map中指攒,lastPlacedIndex=0
newChild中第一個位置的D在oldFiber中的index=3 > lastPlacedIndex=0不需要移動慷妙,lastPlacedIndex=3
newChild中第二個位置的A在oldFiber中的index=0 < lastPlacedIndex=3,移動到最后
newChild中第三個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最后
-
newChild中第四個位置的C在oldFiber中的index=2 < lastPlacedIndex=3,移動到最后
看圖更直觀
_16代碼如下:
function placeChild(newFiber, lastPlacedIndex, newIndex) { newFiber.index = newIndex; if (!shouldTrackSideEffects) { return lastPlacedIndex; } var current = newFiber.alternate; if (current !== null) { var oldIndex = current.index; if (oldIndex < lastPlacedIndex) { //oldIndex小于lastPlacedIndex的位置 則將節(jié)點插入到最后 newFiber.flags = Placement; return lastPlacedIndex; } else { return oldIndex;//不需要移動 lastPlacedIndex = oldIndex; } } else { //新增插入 newFiber.flags = Placement; return lastPlacedIndex; } }
```
function reconcileChildrenArray(
returnFiber: Fiber,//父fiber節(jié)點
currentFirstChild: Fiber | null,//childs中第一個節(jié)點
newChildren: Array<*>,//新節(jié)點數(shù)組 也就是jsx數(shù)組
lanes: Lanes,//lane相關(guān) 第12章介紹
): Fiber | null {
let resultingFirstChild: Fiber | null = null;//diff之后返回的第一個節(jié)點
let previousNewFiber: Fiber | null = null;//新節(jié)點中上次對比過的節(jié)點
let oldFiber = currentFirstChild;//正在對比的oldFiber
let lastPlacedIndex = 0;//上次可復用的節(jié)點位置 或者oldFiber的位置
let newIdx = 0;//新節(jié)點中對比到了的位置
let nextOldFiber = null;//正在對比的oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {//第一次遍歷
if (oldFiber.index > newIdx) {//nextOldFiber賦值
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(//更新節(jié)點,如果key不同則newFiber=null
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;//跳出第一次遍歷
}
if (shouldTrackSideEffects) {//檢查shouldTrackSideEffects
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//標記節(jié)點插入
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);//將oldFiber中沒遍歷完的節(jié)點標記為DELETION
return resultingFirstChild;
}
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {//第2次遍歷
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//插入新增節(jié)點
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 將剩下的oldFiber加入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {//第三次循環(huán) 處理節(jié)點移動
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(//刪除找到的節(jié)點
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//標記為插入的邏輯
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
//刪除existingChildren中剩下的節(jié)點
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
```