人人都能讀懂的react源碼解析(大廠高薪必備)
8.diff算法(媽媽再也不擔心我的diff面試了)
視頻課程&調試demos
視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。
視頻課程:進入課程
demos:demo
課程結構:
- 開篇(聽說你還在艱難的啃react源碼)
- react心智模型(來來來,讓大腦有react思維吧)
- Fiber(我是在內存中的dom)
- 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
- state更新流程(setState里到底發生了什么)
- render階段(厲害了,我有創建Fiber的技能)
- commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
- diff算法(媽媽再也不擔心我的diff面試了)
- hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
- scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
- concurrent mode(並發模式是什么樣的)
- 手寫迷你react(短小精悍就是我)
在render階段更新Fiber節點時,我們會調用reconcileChildFibers對比current Fiber和jsx對象構建workInProgress Fiber,這里current Fiber是指當前dom對應的fiber樹,jsx是class組件render方法或者函數組件的返回值。
在reconcileChildFibers中會根據newChild的類型來進入單節點的diff或者多節點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:
//單一節點diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
}
}
//...
if (isArray(newChild)) {
//多節點diff
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 刪除節點
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
diff過程的主要流程如下圖:
我們知道對比兩顆樹的復雜度本身是O(n3),對我們的應用來說這個是不能承受的量級,react為了降低復雜度,提出了三個前提:
-
只對同級比較,跨層級的dom不會進行復用
-
不同類型節點生成的dom樹不同,此時會直接銷毀老節點及子孫節點,並新建節點
-
可以通過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,因為節點的更新前后文本節點不同,導致他們都不能復用,所以會銷毀之前的節點,並新建節點,但是現在有key了,b中的節點會在老的a中尋找key相同的節點嘗試復用,最后發現只是交換位置就可以完成更新,具體對比過程后面會講到。
單節點diff
單點diff有如下幾種情況:
- key和type相同表示可以復用節點
- key不同直接標記刪除節點,然后新建節點
- key相同type不同,標記刪除該節點和兄弟節點,然后新創建節點
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber { const key = element.key; let child = currentFirstChild; //child節點不為null執行對比 while (child !== null) { // 1.比較key if (child.key === key) { // 2.比較type switch (child.tag) { //... default: { if (child.elementType === element.type) { // type相同則可以復用 返回復用的節點 return existing; } // type不同跳出 break; } } //key相同,type不同則把fiber及和兄弟fiber標記刪除 deleteRemainingChildren(returnFiber, child); break; } else { //key不同直接標記刪除該節點 deleteChild(returnFiber, child); } child = child.sibling; } //新建新Fiber }
多節點diff
多節點diff比較復雜,我們分三種情況進行討論,其中a表示更新前的節點,b表示更新后的節點
-
屬性變化
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> </> );
-
新增節點
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> </> );
-
節點刪除
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> </> );
-
節點位置變化
const a = ( <> <p key="0">0</p> <p key="1">1</p> </> ); const b = ( <> <p key="1">1</p> <p key="0">0</p> </> );
在源碼中多節點diff會經歷三次遍歷,第一次遍歷處理節點的更新(包括props更新和type更新和刪除),第二次遍歷處理其他的情況(節點新增),其原因在於在大多數的應用中,節點更新的頻率更加頻繁,第三次處理位節點置改變
-
第一次遍歷
因為老的節點存在於current Fiber中,所以它是個鏈表結構,還記得Fiber雙緩存結構嘛,節點通過child、return、sibling連接,而newChildren存在於jsx當中,所以遍歷對比的時候,首先讓newChildren[i]
與
oldFiber對比,然后讓i++、nextOldFiber = oldFiber.sibling。在第一輪遍歷中,會處理三種情況,其中第1,2兩種情況會結束第一次循環- key不同,第一次循環結束
- newChildren或者oldFiber遍歷完,第一次循環結束
- key同type不同,標記oldFiber為DELETION
- key相同type相同則可以復用
newChildren遍歷完,oldFiber沒遍歷完,在第一次遍歷完成之后將oldFiber中沒遍歷完的節點標記為DELETION,即刪除的DELETION Tag
-
第二次遍歷
第二次遍歷考慮三種情況
1. newChildren和oldFiber都遍歷完:多節點diff過程結束 2. newChildren沒遍歷完,oldFiber遍歷完,將剩下的newChildren的節點標記為Placement,即插入的Tag
- newChildren和oldFiber沒遍歷完,則進入節點移動的邏輯
-
第三次遍歷
主要邏輯在placeChild函數中,例如更新前節點順序是ABCD,更新后是ACDB
-
newChild中第一個位置的A和oldFiber第一個位置的A,key相同可復用,lastPlacedIndex=0
-
newChild中第二個位置的C和oldFiber第二個位置的B,key不同跳出第一次循環,將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,移動到最后
看圖更直觀
-
例如更新前節點順序是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,移動到最后
看圖更直觀
代碼如下:
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的位置 則將節點插入到最后 newFiber.flags = Placement; return lastPlacedIndex; } else { return oldIndex;//不需要移動 lastPlacedIndex = oldIndex; } } else { //新增插入 newFiber.flags = Placement; return lastPlacedIndex; } }
function reconcileChildrenArray( returnFiber: Fiber,//父fiber節點 currentFirstChild: Fiber | null,//childs中第一個節點 newChildren: Array<*>,//新節點數組 也就是jsx數組 lanes: Lanes,//lane相關 第12章介紹 ): Fiber | null { let resultingFirstChild: Fiber | null = null;//diff之后返回的第一個節點 let previousNewFiber: Fiber | null = null;//新節點中上次對比過的節點 let oldFiber = currentFirstChild;//正在對比的oldFiber let lastPlacedIndex = 0;//上次可復用的節點位置 或者oldFiber的位置 let newIdx = 0;//新節點中對比到了的位置 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(//更新節點,如果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);//標記節點插入 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } if (newIdx === newChildren.length) { deleteRemainingChildren(returnFiber, oldFiber);//將oldFiber中沒遍歷完的節點標記為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);//插入新增節點 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++) {//第三次循環 處理節點移動 const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { existingChildren.delete(//刪除找到的節點 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中剩下的節點 existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; }
-