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

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