自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從源碼層面理解 React 是如何做 Diff 的

開(kāi)發(fā) 前端
ReconcileChildFibers 是 ChildReconciler 方法內(nèi)部定義的方法,通過(guò)調(diào)用 ChildReconciler 方法,并傳入一個(gè) shouldTrackSideEffects 參數(shù)返回。這樣做是為了根據(jù)不同使用場(chǎng)景 ,產(chǎn)生不同的效果。

大家好,我是前端西瓜哥。今天帶帶大家來(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ā)生的邏輯有:

  1. 有一個(gè)指針走完,即 nextFiber 變成 null 或 newIdx 大于 newChildren.length,循環(huán)結(jié)束。
  2. 如果 key 不同,就會(huì)結(jié)束遍歷(在源碼中的體現(xiàn)是updateSlot() 返回 null 賦值給 newFiber,然后就 break 跳出循環(huán))。
  3. 如果 key 相同,但 type 不同,說(shuō)明這個(gè)舊節(jié)點(diǎn)是不能用的了,給它 打上 “刪除” 標(biāo)記,然后繼續(xù)遍歷。
  4. 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ù)雜的。

責(zé)任編輯:姜華 來(lái)源: 前端西瓜哥
相關(guān)推薦

2022-12-23 08:34:30

HookReact

2022-08-03 09:11:31

React性能優(yōu)化

2012-03-12 16:42:54

測(cè)試

2024-12-05 09:45:25

Reactdiff 算法前端開(kāi)發(fā)

2024-04-22 08:26:37

協(xié)同編輯FigmaOT 算法

2024-01-15 07:42:37

Figma協(xié)同編輯算法

2015-08-11 09:13:16

2048WEB開(kāi)發(fā)

2014-04-15 13:16:00

Code Review

2011-08-01 09:08:49

程序員

2023-08-07 08:01:15

2021-04-15 09:07:52

hotspotJavaC++

2021-05-13 08:00:00

軟件測(cè)試程序IT

2016-10-26 20:49:24

ReactJavascript前端

2017-11-16 21:21:18

DevOps測(cè)試軟件開(kāi)發(fā)

2021-07-06 10:03:05

軟件開(kāi)發(fā) 技術(shù)

2019-09-15 14:07:49

2015-07-30 11:21:16

代碼審查

2022-08-29 08:08:58

SQLOracleCPU

2019-01-03 14:00:37

降價(jià)青云全棧云

2017-04-05 08:39:20

機(jī)器學(xué)習(xí)模型梯度下降法
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)