React 源碼中最重要的部分,你知道有哪些嗎?
無論是并發模式,還是同步模式,最終要生成新的 Fiber Tree,都是通過遍歷 workInProgress 的方式去執行 performUnitOfWork。
// 并發模式
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 表示當前正在執行的 Fiber 節點,他會在遞歸的過程中不斷改變指向,這里要結合我們之前章節中分享過的 Fiber Tree 的鏈表結構來理解。
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
一、performUnitOfWork
該方法主要用于創建 Fiber Tree,是否理解 Fiber Tree 的構建過程,跟我們是否能做好性能優化有非常直接的關系,因此對我而言,這是 React 源碼中最重要的一個部分。
從他的第一行代碼我們就能知道,Fiber Tree 的創建是依賴于雙緩存策略。上一輪構建完成的 Fiber tree,在代碼中用 current 來表示。
正在構建中的 Fiber tree,在代碼中用 workInProgress 來表示,并且他們之間同層節點都用 alternate 相互指向。
current.alternate = workInProgress;
workInProgress.alternate = current;
workInProgress 會基于 current 構建。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
...
整體的思路是從 current[rootFiber] 樹往下執行深度遍歷,在遍歷的過程中,會根據 key、props、context、state 等條件進行判斷,判斷結果如果發現節點沒有發生變化,那么就復用 current 的節點,如果發生了變化,則重新創建 Fiber 節點,并標記需要修改的類型,用于傳遞給 commitRoot。
二、beginWork
每一個被遍歷到的 Fiber 節點,會執行 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);
}
該方法根據傳入的 Fiber 節點創建子節點,并將這兩個節點連接起來。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {...}
React 在 ReactFiberBeginWork.new.js 模塊中維護了一個全局的 didReceiveUpdate 變量,來表示當前節點是否需要更新。
let didReceiveUpdate: boolean = false;
在 beginWork 的執行過程中,會經歷一些判斷來確認 didReceiveUpdate 的值,從而判斷該 Fiber 節點是否需要重新執行。
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 是否發生了變化。當他們其中一個變化時,則將 didReceiveUpdate 設置為 true。
這里的 hasLegacyContextChanged() 兼容的是舊版本 的 context,新版本的 context 是否發生變化會反應到 pending update 中,也就是使用下面的 checkScheduledUpdateOrContext 來查看是否有更新的調度任務。
當 props 和 context 都沒有發生變化,并且也不存在對應的調度任務時,將其設置為 false。
如果有 state/context 發生變化,則會存在調度任務。
} 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,
);
}
這里有一個很關鍵的點,就在于當方法進入到 attemptEarlyBailoutIfNoScheduledUpdate 去判斷子節點是否可以 bailout 時,他并沒有比較子節點的 props。
核心的邏輯在 bailoutOnAlreadyFinishedWork 中。
{
...
// 判斷子節點是否有 pending 任務要做
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;
}
所以這里有一個很重要的思考就是為什么判斷子節點是否發生變化時,并沒有去比較 props,這是性能優化策略的關鍵一步,結合我們之前講的性能優化策略去理解,你就能知道答案。
回到 beginWork, 后續的邏輯會根據不同的 tag,創建不同類型的 Fiber 節點。
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,
);
}
}
// ... 其他類型
我們重點關注 updateFunctionComponent 的執行邏輯,可以發現,當 didReceiveUpdate 為 false 時,會執行 bailout 跳過創建過程。
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
如果無法 bailout,則最后執行 reconcileChildren 創建新的子節點。
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
另外我們還應該關注 updateMemoComponent 中的邏輯。該邏輯通過淺比較函數 shallowEqual 來比較更新前后兩個 props 的差異。當比較結果為 true 時,也是調用 bailout 跳過創建。
而不是沿用 didReceiveUpdate 的結果。
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 執行過程中,當發現當前節點已經沒有子節點了,就會調用 completeUnitOfWork 方法。
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
該方法主要用于執行 completeWork。completeWork 主要的作用是用于創建與 Fiber 節點對應的 DOM 節點。
這里創建的 DOM 節點并沒有插入到 HTML 中,還存在于內存里。
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
由于 completeWork 的執行是從葉子節點,往根節點執行,因此,每次我們將新創建的節點 append 到父節點,執行到最后 rootFiber 時,一個完整的 DOM 樹就已經構建完成了。
completeWork 的執行順序是一個回溯的過程。
當然,Fiber 節點與 DOM 節點之間,也會保持一一對應的引用關系,因此在更新階段,我們能夠輕易的判斷和復用已經存在的 DOM 節點從而避免重復創建。
四、遍歷順序
beginWork 和 completeWork 的執行順序理解起來比較困難,為了便于理解,我們這里用一個圖示來表達。
例如有這樣一個結構的節點。
<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 的執行是按照 Fiber 節點的鏈表深度遍歷執行。
completeWork 則是當 fiber.next === null 時開始執行,他一個從葉子節點往根節點執行的回溯過程。當葉子節點被執行過后,則對葉子節點的父節點執行 completeWork。
下圖就是上面 demo 的執行順序。
其中藍色圓代表對應節點的 beginWork 執行。黃色圓代表對應節點的 completeWork 執行。
五、總結
beginWork 與 completeWork 的執行是 React 源碼中最重要的部分,理解他們的核心邏輯能有效幫助我們做好項目的性能優化。因此在學習他們的過程中,應該結合實踐去思考優化策略。
不過性能優化的方式在我們之前的章節中已經詳細介紹過,因此這里帶大家閱讀源碼更多的是做一個驗證,去揭開源碼的神秘面紗。
到這篇文章這里,React 原理的大多數重要邏輯我們在知命境的文章都已經給大家分享過了,其中包括同步更新邏輯,異步更新邏輯,任務優先級隊列,任務調度,Fiber 中的各種鏈表結構,各種比較方式的成本,包括本文介紹的 Fiber tree 的構建過程,大家可以把這些零散的文章串起來總結一下,有能力的可以自己在閱讀源碼時結合我分享的內容進一步擴展和完善。
閱讀源碼是一個高投入,低回報的過程,希望我的這些文章能有效幫助大家以更低的時間成本獲得更高的知識回報。