成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

大家好,我是前端西瓜哥。今天帶帶大家來分析React源碼,理解單節(jié)點 diff 和多節(jié)點 diff 的具體實現(xiàn)。

React 的版本為 18.2.0

reconcileChildFibers

React 的節(jié)點對比邏輯是在 reconcileChildFibers 方法中實現(xiàn)的。

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

因為一個組件的更新和掛載的流程不同的。比如掛載會執(zhí)行掛載的生命周期函數(shù),更新則不會。

// reconcileChildFibers,和內(nèi)部方法同名
export const reconcileChildFibers = ChildReconciler(true);

// mountChildFibers 是在一個節(jié)點從無到有的情況下調(diào)用
export const mountChildFibers = ChildReconciler(false);

reconcileChildFibers 的核心實現(xiàn):

function reconcileChildFibers(
returnFiber,
currentFirstChild,
newChild,
lanes,
) {
// newChild 可能是數(shù)組或?qū)ο?/span>
// 如果是數(shù)組,那它的 $$typeof 就是 undefined
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 單節(jié)點 diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// ...
}

// 多節(jié)點 diff
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
}

newChild 是在組件 render 時得到 ReactElement,通過訪問組件的 props.children 得到。

如果 newChild 是對象(非數(shù)組),會 調(diào)用 reconcileSingleElement(普通元素的情況),做單個節(jié)點的對比。

如果是數(shù)組時,就會 調(diào)用 reconcileChildrenArray,進(jìn)行多節(jié)點的 diff。

更新和掛載的邏輯有點不同,后面都會用 “更新” 的場景進(jìn)行講解。

單節(jié)點 diff

先看看 單節(jié)點 diff。

需要注意的是,這里的 “單節(jié)點” 指的是新生成的 ReactElement 是單個的。只要新節(jié)點是數(shù)組就不算單節(jié)點,即使數(shù)組長度只為 1。此外舊節(jié)點可能是有兄弟節(jié)點的(sibling 不為 null)。

fiber 對象是通過鏈表來表示節(jié)點之間的關(guān)系的,它的 sibling 指向它的下一個兄弟節(jié)點,index 表示在兄弟節(jié)點中的位置。

ReactElement 則是對象或數(shù)組的形式,通過 React.createElement() 生成。

單節(jié)點 diff 對應(yīng) reconcileSingleElement 方法,其核心實現(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 相同,且類型相同(比如新舊都是 div 類型)
// 則走 “更新” 邏輯
if (child.elementType === elementType) {
// 【分支 1】
// 將舊節(jié)點后所有的 sibling 打上刪除 tag
deleteRemainingChildren(returnFiber, child.sibling);

// 創(chuàng)建 WorkInProgress,也就是原來 fiber 的替身啦
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
} else {
//【分支 2】
deleteRemainingChildren(returnFiber, child);
break;
}
}
// 當(dāng)前節(jié)點 key 不匹配,將它標(biāo)記為待刪除
else {
// 【分支 3】
deleteChild(returnFiber, child);
}
// 取下一個兄弟節(jié)點,繼續(xù)做對比
child = child.sibling;
}

// 執(zhí)行到這里說明沒發(fā)現(xiàn)可復(fù)用節(jié)點,需要創(chuàng)建一個 fiber 出來
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}

currentFirstChild 是更新前的節(jié)點,它是以鏈表的保存的,它的 sibling 指向它的下一個兄弟節(jié)點。

分支很多,下面我們進(jìn)行詳細(xì)地分析。

分支 1:key 相同且 type 相同

當(dāng)發(fā)現(xiàn) key 相同時,React 會嘗試復(fù)用組件。新舊節(jié)點的 key 都沒有設(shè)置的話,會設(shè)置為 null,如果新舊節(jié)點的 key 都為 null,會認(rèn)為相等。

此外還要判斷新舊類型是否相同(比如都是 div),因為類型都不同了,是無法復(fù)用的。

如果都滿足,就會將舊 fiber 的后面的兄弟節(jié)點都標(biāo)記為待刪除,具體是調(diào)用 deleteRemainingChildren() 方法,它會在父 fiber 的 deletions 數(shù)組上,添加指定的子 fiber 和它之后的所有兄弟節(jié)點,作為刪除標(biāo)記。

之后的 commit 階段會再進(jìn)行正式的刪除,再執(zhí)行一些調(diào)用生命周期函數(shù)等邏輯。

useFiber() 會創(chuàng)建舊的 fiber 的替身,更新到 fiber 的 alternate 屬性上,最后這個 useFiber 返回這個 alternate。然后直接 return,結(jié)束這個方法。

分支 2:key 相同但 type 不同

type 不同是無法復(fù)用的,如果 type 不同但 key 卻相同,React 會認(rèn)為沒有匹配的可復(fù)用節(jié)點了。直接就將剩下的兄弟節(jié)點標(biāo)記為刪除,然后結(jié)束循環(huán)。

分支 3:key 不匹配

key 不同,用 deleteChild() 方法將當(dāng)前的 fiber 節(jié)點標(biāo)記為待刪除,取出下一個兄弟節(jié)點再和新節(jié)點再比較,不斷循環(huán),直到匹配到其中一種分支為止。

以上就是三個分支。

如果能走到循環(huán)結(jié)束,說明沒能找到能復(fù)用的 fiber,就會根據(jù) ReactElement 調(diào)用 createFiberFromElement() 方法創(chuàng)建一個新的 fiber,然后返回它。

外部會拿到這個 fiber,調(diào)用 placeSingleChild() 將其 打上待更新 tag。

reconcileChildrenArray

然后是 多節(jié)點 diff。

對應(yīng) ReactElement 為數(shù)組的場景,這種場景的算法實現(xiàn)要復(fù)雜的多。

多節(jié)點 diff 對應(yīng) reconcileChildrenArray 方法,因為算法比較復(fù)雜,先不直接貼比較完整的代碼,而是分成幾個階段去一點點講解。

多節(jié)點的 diff 分 4 個階段,下面細(xì)說。

階段1:同時從左往右遍歷

圖片

舊 fiber 和 element 各自的指針一起從左往右走。指針分別為 nextFiber 和 newIdx,從左往右不斷遍歷。

遍歷中發(fā)生的邏輯有:

  1. 有一個指針走完,即 nextFiber 變成 null 或 newIdx 大于 newChildren.length,循環(huán)結(jié)束。
  2. 如果 key 不同,就會結(jié)束遍歷(在源碼中的體現(xiàn)是updateSlot() 返回 null 賦值給 newFiber,然后就 break 跳出循環(huán))。
  3. 如果 key 相同,但 type 不同,說明這個舊節(jié)點是不能用的了,給它 打上 “刪除” 標(biāo)記,然后繼續(xù)遍歷。
  4. key 相同,type 也相同,復(fù)用節(jié)點。對于普通元素類型,最終會調(diào)用 updateElement 方法。

updateElement 方法會判斷 fiber 和 element 的類型是否相同,如果相同,會給 fiber 的 alternate 生成一個 workInProcess(替身) fiber 返回,否則 創(chuàng)建一個新的 fiber 返回。它們會帶上新的 pendingProps 屬性。

function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節(jié)點數(shù)組
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;
}
// 更新節(jié)點(或生成新的待插入節(jié)點)
// 方法內(nèi)部會判斷 key 是否相等,不相等會返回 null。
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);

// 如果當(dāng)前新舊節(jié)點不匹配,就跳出循環(huán)
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}

if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// newFiber 不是基于 oldFiber 的 alternate 創(chuàng)建的
// 說明 oldFiber 要銷毀掉,要打上 “刪除” 標(biāo)記
deleteChild(returnFiber, oldFiber);
}
}

// 打 “place” 標(biāo)記
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
}

階段 2:新節(jié)點遍歷完的情況

跳出循環(huán)后,我們先看 新節(jié)點數(shù)組是否遍歷完(newIdx 是否等于 newChildren.length)。

是的話,就將舊節(jié)點中剩余的所有節(jié)點編輯為 “刪除”,然后直接結(jié)束整個函數(shù)。

function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節(jié)點數(shù)組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...

// 【2】如果新節(jié)點遍歷完,將舊節(jié)點剩余節(jié)點全都標(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é)點遍歷完,新節(jié)點沒遍歷完的情況

如果是舊節(jié)點遍歷完了,但新節(jié)點沒有遍歷完,就將新節(jié)點中的剩余節(jié)點,根據(jù) element 構(gòu)建為 fiber。

function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節(jié)點數(shù)組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...

// 【2】如果新節(jié)點遍歷完,將舊節(jié)點剩余節(jié)點全都標(biāo)記為刪除
// ...

// 【3】如果舊節(jié)點遍歷完了,但新節(jié)點沒有遍歷完,根據(jù)剩余新節(jié)點生成新 fiber
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 通過 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 彼此連起來
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
// 返回新建 fiber 中的第一個
return resultingFirstChild;
}
}

階段 4:使用 map 高效匹配新舊節(jié)點進(jìn)行更新

【4】如果新舊節(jié)點都沒遍歷完,那我們會調(diào)用 mapRemainingChildren 方法,先將剩余的舊節(jié)點,放到 Map 映射中,以便快速訪問。

map 中會優(yōu)先使用 fiber.key(保證會轉(zhuǎn)換為字符串)作為鍵;如果 fiber.key 是 null,則使用 fiber.index(數(shù)值類型),key 和 index 的值是不會沖突的。值自然就是 fiber 對象本身。

然后就是遍歷剩余的新節(jié)點,調(diào)用 updateFromMap 方法,從映射表中找到對應(yīng)的舊節(jié)點,和新節(jié)點進(jìn)行對比更新。

遍歷完后就是收尾工作了,map 中剩下的就是沒能匹配的舊節(jié)點,給它們打上 “刪除” 標(biāo)記。

function reconcileChildrenArray(
returnFiber,
currentFirstChild, // 舊的 fiber
newChildren, // 新節(jié)點數(shù)組
lanes,
) {
// 【1】分別從左往右遍歷對比更新
// ...

// 【2】如果新節(jié)點遍歷完,將舊節(jié)點剩余節(jié)點全都標(biāo)記為刪除
// ...

// 【3】如果舊節(jié)點遍歷完了,但新節(jié)點沒有遍歷完,根據(jù)剩余新節(jié)點生成新 fiber
// ...

// 【4】剩余舊節(jié)點放入 map 中,再遍歷快速訪問,快速進(jìn)行新舊節(jié)點匹配更新。
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 中的對應(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é)點的頭個節(jié)點
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

// 收尾工作,將沒能匹配的舊節(jié)點打上 “刪除” 標(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é)尾

有點復(fù)雜的。

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

2022-12-23 08:34:30

HookReact

2022-08-03 09:11:31

React性能優(yōu)化

2012-03-12 16:42:54

測試

2024-12-05 09:45:25

Reactdiff 算法前端開發(fā)

2024-04-22 08:26:37

協(xié)同編輯FigmaOT 算法

2024-01-15 07:42:37

Figma協(xié)同編輯算法

2015-08-11 09:13:16

2048WEB開發(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

軟件測試程序IT

2017-11-16 21:21:18

DevOps測試軟件開發(fā)

2021-07-06 10:03:05

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

2016-10-26 20:49:24

ReactJavascript前端

2019-09-15 14:07:49

2015-07-30 11:21:16

代碼審查

2019-01-03 14:00:37

降價青云全棧云

2022-08-29 08:08:58

SQLOracleCPU

2017-04-05 08:39:20

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

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

主站蜘蛛池模板: 久久综合一区二区 | 欧美精品在线播放 | 三级黄视频在线观看 | www.99热| 国产精品久久久久久久久久 | 99久久亚洲 | 久久青 | 色婷婷综合久久久中字幕精品久久 | www.久久久.com | 九九久久免费视频 | 精品欧美一区二区三区精品久久 | 久久国产精品一区二区三区 | 精品亚洲一区二区三区 | 亚洲视频一区二区三区四区 | 国产美女在线观看 | 成年人在线 | 久久久久久久一区 | 女生羞羞网站 | 九九综合九九 | 色婷婷久久久久swag精品 | 精品福利在线 | 国产精品夜色一区二区三区 | 一区二区三区视频在线免费观看 | 亚洲成人av一区二区 | 精品一区二区三区91 | 成人视屏在线观看 | 国产一级黄色网 | 欧美日韩福利视频 | 91视频18| 久久久99精品免费观看 | 欧美性影院 | 国产美女网站 | 日韩在线中文字幕 | 日本成人久久 | 国产精品99久久久久久大便 | 日韩中文字幕 | 亚洲精品久久久久久下一站 | 九九九精品视频 | 黄a大片| 亚洲国产成人在线观看 | 羞羞视频网站 |