React 源碼中最重要的部分,你知道有哪些嗎?
無論是并發(fā)模式,還是同步模式,最終要生成新的 Fiber Tree,都是通過遍歷 workInProgress 的方式去執(zhí)行 performUnitOfWork。
// 并發(fā)模式
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
// 同步
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
需要特別注意的是這里的 workInProgress 表示當前正在執(zhí)行的 Fiber 節(jié)點,他會在遞歸的過程中不斷改變指向,這里要結(jié)合我們之前章節(jié)中分享過的 Fiber Tree 的鏈表結(jié)構(gòu)來理解。
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
一、performUnitOfWork
該方法主要用于創(chuàng)建 Fiber Tree,是否理解 Fiber Tree 的構(gòu)建過程,跟我們是否能做好性能優(yōu)化有非常直接的關(guān)系,因此對我而言,這是 React 源碼中最重要的一個部分。
從他的第一行代碼我們就能知道,F(xiàn)iber Tree 的創(chuàng)建是依賴于雙緩存策略。上一輪構(gòu)建完成的 Fiber tree,在代碼中用 current 來表示。
正在構(gòu)建中的 Fiber tree,在代碼中用 workInProgress 來表示,并且他們之間同層節(jié)點都用 alternate 相互指向。
current.alternate = workInProgress;
workInProgress.alternate = current;
workInProgress 會基于 current 構(gòu)建。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
...
整體的思路是從 current[rootFiber] 樹往下執(zhí)行深度遍歷,在遍歷的過程中,會根據(jù) key、props、context、state 等條件進行判斷,判斷結(jié)果如果發(fā)現(xiàn)節(jié)點沒有發(fā)生變化,那么就復(fù)用 current 的節(jié)點,如果發(fā)生了變化,則重新創(chuàng)建 Fiber 節(jié)點,并標記需要修改的類型,用于傳遞給 commitRoot。
二、beginWork
每一個被遍歷到的 Fiber 節(jié)點,會執(zhí)行 beginWork 方法。
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
該方法根據(jù)傳入的 Fiber 節(jié)點創(chuàng)建子節(jié)點,并將這兩個節(jié)點連接起來。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {...}
React 在 ReactFiberBeginWork.new.js 模塊中維護了一個全局的 didReceiveUpdate 變量,來表示當前節(jié)點是否需要更新。
let didReceiveUpdate: boolean = false;
在 beginWork 的執(zhí)行過程中,會經(jīng)歷一些判斷來確認 didReceiveUpdate 的值,從而判斷該 Fiber 節(jié)點是否需要重新執(zhí)行。
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
這里比較的是 props 和 context 是否發(fā)生了變化。當他們其中一個變化時,則將 didReceiveUpdate 設(shè)置為 true。
這里的 hasLegacyContextChanged() 兼容的是舊版本 的 context,新版本的 context 是否發(fā)生變化會反應(yīng)到 pending update 中,也就是使用下面的 checkScheduledUpdateOrContext 來查看是否有更新的調(diào)度任務(wù)。
當 props 和 context 都沒有發(fā)生變化,并且也不存在對應(yīng)的調(diào)度任務(wù)時,將其設(shè)置為 false。
如果有 state/context 發(fā)生變化,則會存在調(diào)度任務(wù)。
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
這里有一個很關(guān)鍵的點,就在于當方法進入到 attemptEarlyBailoutIfNoScheduledUpdate 去判斷子節(jié)點是否可以 bailout 時,他并沒有比較子節(jié)點的 props。
核心的邏輯在 bailoutOnAlreadyFinishedWork 中。
{
...
// 判斷子節(jié)點是否有 pending 任務(wù)要做
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
return null
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
所以這里有一個很重要的思考就是為什么判斷子節(jié)點是否發(fā)生變化時,并沒有去比較 props,這是性能優(yōu)化策略的關(guān)鍵一步,結(jié)合我們之前講的性能優(yōu)化策略去理解,你就能知道答案。
回到 beginWork, 后續(xù)的邏輯會根據(jù)不同的 tag,創(chuàng)建不同類型的 Fiber 節(jié)點。
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
// ...
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
}
// ... 其他類型
我們重點關(guān)注 updateFunctionComponent 的執(zhí)行邏輯,可以發(fā)現(xiàn),當 didReceiveUpdate 為 false 時,會執(zhí)行 bailout 跳過創(chuàng)建過程。
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
如果無法 bailout,則最后執(zhí)行 reconcileChildren 創(chuàng)建新的子節(jié)點。
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
另外我們還應(yīng)該關(guān)注 updateMemoComponent 中的邏輯。該邏輯通過淺比較函數(shù) shallowEqual 來比較更新前后兩個 props 的差異。當比較結(jié)果為 true 時,也是調(diào)用 bailout 跳過創(chuàng)建。
而不是沿用 didReceiveUpdate 的結(jié)果。
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
二、completeWork
在 performUnitOfWork 執(zhí)行過程中,當發(fā)現(xiàn)當前節(jié)點已經(jīng)沒有子節(jié)點了,就會調(diào)用 completeUnitOfWork 方法。
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
該方法主要用于執(zhí)行 completeWork。completeWork 主要的作用是用于創(chuàng)建與 Fiber 節(jié)點對應(yīng)的 DOM 節(jié)點。
這里創(chuàng)建的 DOM 節(jié)點并沒有插入到 HTML 中,還存在于內(nèi)存里。
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
由于 completeWork 的執(zhí)行是從葉子節(jié)點,往根節(jié)點執(zhí)行,因此,每次我們將新創(chuàng)建的節(jié)點 append 到父節(jié)點,執(zhí)行到最后 rootFiber 時,一個完整的 DOM 樹就已經(jīng)構(gòu)建完成了。
completeWork 的執(zhí)行順序是一個回溯的過程。
當然,F(xiàn)iber 節(jié)點與 DOM 節(jié)點之間,也會保持一一對應(yīng)的引用關(guān)系,因此在更新階段,我們能夠輕易的判斷和復(fù)用已經(jīng)存在的 DOM 節(jié)點從而避免重復(fù)創(chuàng)建。
四、遍歷順序
beginWork 和 completeWork 的執(zhí)行順序理解起來比較困難,為了便于理解,我們這里用一個圖示來表達。
例如有這樣一個結(jié)構(gòu)的節(jié)點。
<div id="root">
<div className="1">
<div className="1-1">1-1</div>
<div className="1-2">1-2</div>
<div className="1-3">
<div className="1-3-1">1-3-1</div>
</div>
</div>
<div className="2">2</div>
<div className="3">3</div>
</div>
beginWork 的執(zhí)行是按照 Fiber 節(jié)點的鏈表深度遍歷執(zhí)行。
completeWork 則是當 fiber.next === null 時開始執(zhí)行,他一個從葉子節(jié)點往根節(jié)點執(zhí)行的回溯過程。當葉子節(jié)點被執(zhí)行過后,則對葉子節(jié)點的父節(jié)點執(zhí)行 completeWork。
下圖就是上面 demo 的執(zhí)行順序。
其中藍色圓代表對應(yīng)節(jié)點的 beginWork 執(zhí)行。黃色圓代表對應(yīng)節(jié)點的 completeWork 執(zhí)行。
五、總結(jié)
beginWork 與 completeWork 的執(zhí)行是 React 源碼中最重要的部分,理解他們的核心邏輯能有效幫助我們做好項目的性能優(yōu)化。因此在學(xué)習他們的過程中,應(yīng)該結(jié)合實踐去思考優(yōu)化策略。
不過性能優(yōu)化的方式在我們之前的章節(jié)中已經(jīng)詳細介紹過,因此這里帶大家閱讀源碼更多的是做一個驗證,去揭開源碼的神秘面紗。
到這篇文章這里,React 原理的大多數(shù)重要邏輯我們在知命境的文章都已經(jīng)給大家分享過了,其中包括同步更新邏輯,異步更新邏輯,任務(wù)優(yōu)先級隊列,任務(wù)調(diào)度,F(xiàn)iber 中的各種鏈表結(jié)構(gòu),各種比較方式的成本,包括本文介紹的 Fiber tree 的構(gòu)建過程,大家可以把這些零散的文章串起來總結(jié)一下,有能力的可以自己在閱讀源碼時結(jié)合我分享的內(nèi)容進一步擴展和完善。
閱讀源碼是一個高投入,低回報的過程,希望我的這些文章能有效幫助大家以更低的時間成本獲得更高的知識回報。