
大家好,我是前端西瓜哥。今天帶帶大家來分析React源碼,理解單節點 diff 和多節點 diff 的具體實現。
React 的版本為 18.2.0
reconcileChildFibers
React 的節點對比邏輯是在 reconcileChildFibers 方法中實現的。
reconcileChildFibers 是 ChildReconciler 方法內部定義的方法,通過調用 ChildReconciler 方法,并傳入一個 shouldTrackSideEffects 參數返回。這樣做是為了根據不同使用場景 ,產生不同的效果。
因為一個組件的更新和掛載的流程不同的。比如掛載會執行掛載的生命周期函數,更新則不會。
// reconcileChildFibers,和內部方法同名
export const reconcileChildFibers = ChildReconciler(true);
// mountChildFibers 是在一個節點從無到有的情況下調用
export const mountChildFibers = ChildReconciler(false);
reconcileChildFibers 的核心實現:
function reconcileChildFibers(
returnFiber,
currentFirstChild,
newChild,
lanes,
) {
// newChild 可能是數組或對象
// 如果是數組,那它的 $$typeof 就是 undefined
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 單節點 diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// ...
}
// 多節點 diff
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
}
newChild 是在組件 render 時得到 ReactElement,通過訪問組件的 props.children 得到。
如果 newChild 是對象(非數組),會 調用 reconcileSingleElement(普通元素的情況),做單個節點的對比。
如果是數組時,就會 調用 reconcileChildrenArray,進行多節點的 diff。
更新和掛載的邏輯有點不同,后面都會用 “更新” 的場景進行講解。
單節點 diff
先看看 單節點 diff。
需要注意的是,這里的 “單節點” 指的是新生成的 ReactElement 是單個的。只要新節點是數組就不算單節點,即使數組長度只為 1。此外舊節點可能是有兄弟節點的(sibling 不為 null)。
fiber 對象是通過鏈表來表示節點之間的關系的,它的 sibling 指向它的下一個兄弟節點,index 表示在兄弟節點中的位置。
ReactElement 則是對象或數組的形式,通過 React.createElement() 生成。
單節點 diff 對應 reconcileSingleElement 方法,其核心實現為:
function reconcileSingleElement(
returnFiber, // 父 fiber
currentFirstChild, // 更新前的 fiber
element, // 新的 ReactElement
) {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
// key 相同,且類型相同(比如新舊都是 div 類型)
// 則走 “更新” 邏輯
if (child.elementType === elementType) {
// 【分支 1】
// 將舊節點后所有的 sibling 打上刪除 tag
deleteRemainingChildren(returnFiber, child.sibling);
// 創建 WorkInProgress,也就是原來 fiber 的替身啦
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
} else {
//【分支 2】
deleteRemainingChildren(returnFiber, child);
break;
}
}
// 當前節點 key 不匹配,將它標記為待刪除
else {
// 【分支 3】
deleteChild(returnFiber, child);
}
// 取下一個兄弟節點,繼續做對比
child = child.sibling;
}
// 執行到這里說明沒發現可復用節點,需要創建一個 fiber 出來
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
currentFirstChild 是更新前的節點,它是以鏈表的保存的,它的 sibling 指向它的下一個兄弟節點。
分支很多,下面我們進行詳細地分析。
分支 1:key 相同且 type 相同
當發現 key 相同時,React 會嘗試復用組件。新舊節點的 key 都沒有設置的話,會設置為 null,如果新舊節點的 key 都為 null,會認為相等。
此外還要判斷新舊類型是否相同(比如都是 div),因為類型都不同了,是無法復用的。
如果都滿足,就會將舊 fiber 的后面的兄弟節點都標記為待刪除,具體是調用 deleteRemainingChildren() 方法,它會在父 fiber 的 deletions 數組上,添加指定的子 fiber 和它之后的所有兄弟節點,作為刪除標記。
之后的 commit 階段會再進行正式的刪除,再執行一些調用生命周期函數等邏輯。
useFiber() 會創建舊的 fiber 的替身,更新到 fiber 的 alternate 屬性上,最后這個 useFiber 返回這個 alternate。然后直接 return,結束這個方法。
分支 2:key 相同但 type 不同
type 不同是無法復用的,如果 type 不同但 key 卻相同,React 會認為沒有匹配的可復用節點了。直接就將剩下的兄弟節點標記為刪除,然后結束循環。
分支 3:key 不匹配
key 不同,用 deleteChild() 方法將當前的 fiber 節點標記為待刪除,取出下一個兄弟節點再和新節點再比較,不斷循環,直到匹配到其中一種分支為止。
以上就是三個分支。
如果能走到循環結束,說明沒能找到能復用的 fiber,就會根據 ReactElement 調用 createFiberFromElement() 方法創建一個新的 fiber,然后返回它。
外部會拿到這個 fiber,調用 placeSingleChild() 將其 打上待更新 tag。
reconcileChildrenArray
然后是 多節點 diff。
對應 ReactElement 為數組的場景,這種場景的算法實現要復雜的多。
多節點 diff 對應 reconcileChildrenArray 方法,因為算法比較復雜,先不直接貼比較完整的代碼,而是分成幾個階段去一點點講解。
多節點的 diff 分 4 個階段,下面細說。
階段1:同時從左往右遍歷

舊 fiber 和 element 各自的指針一起從左往右走。指針分別為 nextFiber 和 newIdx,從左往右不斷遍歷。
遍歷中發生的邏輯有:
- 有一個指針走完,即 nextFiber 變成 null 或 newIdx 大于 newChildren.length,循環結束。
- 如果 key 不同,就會結束遍歷(在源碼中的體現是updateSlot() 返回 null 賦值給 newFiber,然后就 break 跳出循環)。
- 如果 key 相同,但 type 不同,說明這個舊節點是不能用的了,給它 打上 “刪除” 標記,然后繼續遍歷。
- key 相同,type 也相同,復用節點。對于普通元素類型,最終會調用 updateElement 方法。
updateElement 方法會判斷 fiber 和 element 的類型是否相同,如果相同,會給 fiber 的 alternate 生成一個 workInProcess(替身) fiber 返回,否則 創建一個新的 fiber 返回。它們會帶上新的 pendingProps 屬性。
function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節點數組
lanes,
) {
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 【1】分別從左往右遍歷對比更新
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) { // 舊 fiber 比新 element 多
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 更新節點(或生成新的待插入節點)
// 方法內部會判斷 key 是否相等,不相等會返回 null。
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// 如果當前新舊節點不匹配,就跳出循環
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// newFiber 不是基于 oldFiber 的 alternate 創建的
// 說明 oldFiber 要銷毀掉,要打上 “刪除” 標記
deleteChild(returnFiber, oldFiber);
}
}
// 打 “place” 標記
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
}
階段 2:新節點遍歷完的情況
跳出循環后,我們先看 新節點數組是否遍歷完(newIdx 是否等于 newChildren.length)。
是的話,就將舊節點中剩余的所有節點編輯為 “刪除”,然后直接結束整個函數。
function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節點數組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...
// 【2】如果新節點遍歷完,將舊節點剩余節點全都標記為刪除
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
}
階段三:舊節點遍歷完,新節點沒遍歷完的情況
如果是舊節點遍歷完了,但新節點沒有遍歷完,就將新節點中的剩余節點,根據 element 構建為 fiber。
function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節點數組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...
// 【2】如果新節點遍歷完,將舊節點剩余節點全都標記為刪除
// ...
// 【3】如果舊節點遍歷完了,但新節點沒有遍歷完,根據剩余新節點生成新 fiber
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 通過 element 創建 fiber
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// fiber 設置 index,并打上 “placement” 標簽
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
// 新建的 fiber 彼此連起來
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
// 返回新建 fiber 中的第一個
return resultingFirstChild;
}
}
階段 4:使用 map 高效匹配新舊節點進行更新
【4】如果新舊節點都沒遍歷完,那我們會調用 mapRemainingChildren 方法,先將剩余的舊節點,放到 Map 映射中,以便快速訪問。
map 中會優先使用 fiber.key(保證會轉換為字符串)作為鍵;如果 fiber.key 是 null,則使用 fiber.index(數值類型),key 和 index 的值是不會沖突的。值自然就是 fiber 對象本身。
然后就是遍歷剩余的新節點,調用 updateFromMap 方法,從映射表中找到對應的舊節點,和新節點進行對比更新。
遍歷完后就是收尾工作了,map 中剩下的就是沒能匹配的舊節點,給它們打上 “刪除” 標記。
function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節點數組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...
// 【2】如果新節點遍歷完,將舊節點剩余節點全都標記為刪除
// ...
// 【3】如果舊節點遍歷完了,但新節點沒有遍歷完,根據剩余新節點生成新 fiber
// ...
// 【4】剩余舊節點放入 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) {
// 是在舊 fiber 上的復用更新,所以需要移除 set 中的對應鍵
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 給 newFiber 打上 “place” 標記
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 給新 fiber 構建成鏈表
// 并維持 resultingFirstChild 指向新生成節點的頭個節點
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 收尾工作,將沒能匹配的舊節點打上 “刪除” 標記
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
結尾
有點復雜的。