React EffectList
什么是 EffectList
一個(gè)由 Fiber 構(gòu)成的單向鏈表。
每個(gè) Fiber 節(jié)點(diǎn)都保存著自己子節(jié)點(diǎn)的 EffectList,F(xiàn)iber 對(duì)象上有三個(gè)指針:firstEffct、lastEffect片仿、nextEffect巷折,分別指向下一個(gè)待處理的 effect fiber甸赃,第一個(gè)和最后一個(gè)待處理的 effect fiber瀑罗。
為什么要有 EffectList
作為 DOM 操作的依據(jù),commit 階段需要找到所有有 effectTag 的 Fiber 節(jié)點(diǎn)并依次執(zhí)行 effectTag 對(duì)應(yīng)操作盾戴。難道需要在 commit 階段再遍歷一次 Fiber 樹(shù)尋找 effectTag !== null 的 Fiber 節(jié)點(diǎn)么寄锐?
這顯然是很低效的。
而 EffectList 就解決了這個(gè)問(wèn)題尖啡,在 Fiber 樹(shù)構(gòu)建過(guò)程中橄仆,每當(dāng)一個(gè) Fiber 節(jié)點(diǎn)的 effectTag 字段不為 NoEffect 時(shí)(代表需要執(zhí)行副作用),就把該 Fiber 節(jié)點(diǎn)添加到 EffectList衅斩,在 Fiber 樹(shù)構(gòu)建完成后沿癞,F(xiàn)iber 樹(shù)的 Effect List 也就構(gòu)建完成
EffectList 的收集
在 completeWork 的上層函數(shù) completeUnitOfWork 中,每個(gè)執(zhí)行完 completeWork 且存在 effectTag 的 Fiber 節(jié)點(diǎn)會(huì)被保存在一條被稱為 effectList 的單向鏈表中矛渴。effectList 中第一個(gè) Fiber 節(jié)點(diǎn)保存在 fiber.firstEffect椎扬,最后一個(gè)元素保存在 fiber.lastEffect。
Fiber 樹(shù)的構(gòu)建是深度優(yōu)先的具温,也就是先向下構(gòu)建子級(jí) Fiber 節(jié)點(diǎn)蚕涤,子級(jí)節(jié)點(diǎn)構(gòu)建完成后,再向上構(gòu)建父級(jí) Fiber 節(jié)點(diǎn)铣猩,所以 EffectList 中總是子級(jí) Fiber 節(jié)點(diǎn)在前面揖铜。
completeUnitOfWork 函數(shù)中所做的工作:
完成該 fiber 節(jié)點(diǎn)的構(gòu)建
將該 fiber 的 effectList 更新到其父 Fiber 節(jié)點(diǎn)上
如果當(dāng)前節(jié)點(diǎn)有 effectTag,則將其加入 effectList
如果有 sibling,移動(dòng)到 next sibling 進(jìn)行同樣的操作
沒(méi)有 sibling 則返回父 fiber
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next = completeWork(current, completedWork, subtreeRenderLanes);
// effect list構(gòu)建
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
// 兄弟元素遍歷再到返返回父級(jí)
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
看一個(gè)例子
<div id="1">
<div id="4" />
<div id="2">
<div id="3" />
</div>
</div>
最終形成的 EffectList 為
firstEffect => div4
lastEffect => div1
因?yàn)?Fiber 樹(shù)的構(gòu)建深度優(yōu)先达皿,所以 div4 先完成 completeWork天吓,構(gòu)建 firstEffect。
EffectList 遍歷是從 firstEffect 開(kāi)始峦椰,通過(guò)每一個(gè)節(jié)點(diǎn)的 nextEffect 找到下一個(gè)節(jié)點(diǎn)龄寞。
firstEffect => div4
div4.nextEffect => div3
div3.nextEffect => div2
div2.nextEffect => div1
所以最終形成一條以 rootFiber.firstEffect 為起點(diǎn)的單向鏈表。
這樣汤功,在 commit 階段只需要遍歷 effectList 就能執(zhí)行所有 effect 了物邑。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
EffectList 的遍歷
commit 階段就會(huì)從 rootFiber.firstEffect 開(kāi)始遍歷這個(gè) effectList 來(lái)執(zhí)行副作用
總結(jié)
在 beginWork 中我們知道有的節(jié)點(diǎn)被打上了 effectTag 的標(biāo)記,有的沒(méi)有滔金,而在 commit 階段時(shí)要遍歷所有包含 effectTag 的 Fiber 來(lái)執(zhí)行對(duì)應(yīng)的增刪改色解,那我們還需要從 Fiber 樹(shù)中找到這些帶 effectTag 的節(jié)點(diǎn)嘛,答案是不需要的餐茵,這里是以空間換時(shí)間科阎,在執(zhí)行 completeUnitOfWork 的時(shí)候遇到了帶 effectTag 的節(jié)點(diǎn),會(huì)將這個(gè)節(jié)點(diǎn)加入一個(gè)叫 effectList 中,所以在 commit 階段只要遍歷 effectList 就可以了(rootFiber.firstEffect.nextEffect 就可以訪問(wèn)帶 effectTag 的 Fiber 了)每個(gè) fiber 節(jié)點(diǎn)上都保存了該 fiber 節(jié)點(diǎn)的子節(jié)點(diǎn)的 effectList忿族,通過(guò) firstEffect锣笨、nextEffect刚梭、LastEffect 來(lái)保存,在 completeWork 的時(shí)候就會(huì)將每個(gè) fiber 的 effectList 更新到其父 Fiber 節(jié)點(diǎn)上票唆,所以 complete 之后,rootFiber 上就保存了完整的 effectList屹徘,我們?cè)?commit 階段就直接遍歷 rootFiber 上的 effectList 來(lái)執(zhí)行副作用即可
EffectList 不是全局變量走趋,只是在 Fiber 樹(shù)創(chuàng)建過(guò)程中,一層層向上收集有 effect 的 Fiber 節(jié)點(diǎn)噪伊,最終的 root 節(jié)點(diǎn)就會(huì)收集到所有有 effect 到 Fiber 節(jié)點(diǎn)簿煌,我們就把這條包含 effect 節(jié)點(diǎn)的鏈表叫做 EffectList。
由于收集的過(guò)程是深度優(yōu)先鉴吹,子級(jí)會(huì)先被收集姨伟,所以遍歷的時(shí)候也會(huì)先操作子級(jí),所以如果有面試官問(wèn)子級(jí)和父級(jí)的生命周期或者 useEffect 誰(shuí)先執(zhí)行豆励,就很清楚的知道會(huì)先執(zhí)行子級(jí)操作了夺荒。
補(bǔ)充
effectTag
當(dāng) reconciler 工作結(jié)束后會(huì)通知 Renderer 需要執(zhí)行的 DOM 操作。要執(zhí)行 DOM 操作的具體類型就保存在 fiber.effectTag 中良蒸。
// DOM需要插入到頁(yè)面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到頁(yè)面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要?jiǎng)h除
export const Deletion = /* */ 0b00000000001000;
初次 Render 時(shí)的 EffectList
在 React 中技扼,會(huì)對(duì)初次 Mount 有一個(gè)性能優(yōu)化,其中的 Fiber 節(jié)點(diǎn)的 effectTag 不會(huì)包含 placement嫩痰,對(duì)應(yīng)的 DOM 節(jié)點(diǎn)不會(huì)遍歷加入 DOM 樹(shù)剿吻,而是在創(chuàng)建 DOM 節(jié)點(diǎn)時(shí)就已經(jīng)加入 DOM 樹(shù)了,只有 rootFiber 節(jié)點(diǎn) FiberRootNode 的 effectTag 會(huì)包含 placement串纺。
EffectList 是不會(huì)包含 root 節(jié)點(diǎn)的丽旅,所以需要將 root 節(jié)點(diǎn)也添加到 EffectList,這樣才會(huì)正確的執(zhí)行 placement纺棺,讓 DOM 樹(shù)在頁(yè)面呈現(xiàn) 榄笙。
let firstEffect;
// 把根節(jié)點(diǎn)finishedWork也連接進(jìn)去
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根節(jié)點(diǎn)沒(méi)有effect.
firstEffect = finishedWork.firstEffect;
}
那么,如果要通知 Renderer 將 Fiber 節(jié)點(diǎn)對(duì)應(yīng)的 DOM 節(jié)點(diǎn)插入頁(yè)面中祷蝌,需要滿足兩個(gè)條件:
fiber.stateNode 存在办斑,即 Fiber 節(jié)點(diǎn)中保存了對(duì)應(yīng)的 DOM 節(jié)點(diǎn)
(fiber.effectTag & Placement) !== 0,即 Fiber 節(jié)點(diǎn)存在 Placement effectTag
我們知道杆逗,mount 時(shí)乡翅,fiber.stateNode === null
,且在reconcileChildren
中調(diào)用的mountChildFibers
不會(huì)為 Fiber 節(jié)點(diǎn)賦值 effectTag罪郊。那么首屏渲染如何完成呢蠕蚜?
針對(duì)第一個(gè)問(wèn)題,fiber.stateNode 會(huì)在 completeWork 中創(chuàng)建悔橄。
第二個(gè)問(wèn)題的答案十分巧妙:假設(shè) mountChildFibers 也會(huì)賦值 effectTag靶累,那么可以預(yù)見(jiàn) mount 時(shí)整棵 Fiber 樹(shù)所有節(jié)點(diǎn)都會(huì)有 Placement effectTag腺毫。那么 commit 階段在執(zhí)行 DOM 操作時(shí)每個(gè)節(jié)點(diǎn)都會(huì)執(zhí)行一次插入操作,這樣大量的 DOM 操作是極低效的挣柬。
為了解決這個(gè)問(wèn)題潮酒,在 mount 時(shí)只有 rootFiber 會(huì)賦值 Placement effectTag,在 commit 階段只會(huì)執(zhí)行一次插入操作邪蛔。