React的更新任務主要是調用壹個叫做workLoop的工作循環去構建workInProgress樹,構建過程分為兩個階段:向下遍歷和向上回溯,向下和向上的過程中會對途徑的每個節點進行beginWork和completeWork。
本文即將提到的beginWork是處理節點更新的入口,它會依據fiber節點的類型去調用不同的處理函數。
React對每個節點進行beginWork操作,進入beginWork後,首先判斷節點及其子樹是否有更新,若有更新,則會在計算新狀態和diff之後生成新的Fiber,然後在新的fiber上標記flags(effectTag),最後return它的子節點,以便繼續針對子節點進行beginWork。若它沒有子節點,則返回null,這樣說明這個節點是末端節點,可以進行向上回溯,進入completeWork階段。
點擊進入React源碼調試倉庫。
beginWork的工作流程如下圖,圖中簡化了流程,只對App節點進行了beginWork處理,其余節點流程相似
通過概述可知beginWork階段的整體工作是去更新節點,並返回子樹,但真正的beginWork函數只是節點更新的入口,不會直接進行更新操作。作為入口,它的職責很明顯,攔截無需更新的節點。同時,它還會將context信息入到棧中(beginWork入棧,completeWork出棧),暫時先不關註。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 獲取workInProgress.lanes,可通過判斷它是否為空去判斷該節點是否需要更新
const updateLanes = workInProgress.lanes;
// 依據current是否存在判斷當前是首次掛載還是後續的更新
// 如果是更新,先看優先級夠不夠,不夠的話就能調用bailoutOnAlreadyFinishedWork
// 復用fiber節點來跳出對當前這個節點的處理了。
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
// 此時無需更新
didReceiveUpdate = false;
switch (workInProgress.tag) {
case HostRoot:
...
case HostComponent:
...
case ClassComponent:
...
case HostPortal:
...
}
// 攔截無需更新的節點
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
} else {
didReceiveUpdate = false;
}
// 代碼走到這裏說明確實要去處理節點了,此時會根據不同fiber的類型
// 去調用它們各自的處理函數
// 先清空workInProgress節點上的lanes,因為更新過程中用不到,
// 在處理完updateQueue之後會重新賦值
workInProgress.lanes = NoLanes;
// 依據不同的節點類型來處理節點的更新
switch (workInProgress.tag) {
case IndeterminateComponent:
...
case LazyComponent:
...
case FunctionComponent:
...
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent:
...
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);
......
}
}
可以看出,壹旦節點進入beginWork,會先去識別該節點是否需要處理,若無需處理,則調用bailoutOnAlreadyFinishedWork
復用節點,否則才真正去更新。
判斷current是否存在。
這首先要理解current是什麽,基於雙緩沖的規則,調度更新時有兩棵樹,展示在屏幕上的current Tree和正在後臺基於current樹構建的
workInProgress Tree。那麽,current和workInProgress可以理解為鏡像的關系。workLoop循環當前遍歷到的workInProgress節點來自於它對應的current節點父級fiber的子節點(即current節點),所以workInProgress節點和current節點也是鏡像的關系。
如果是首次渲染,對具體的workInProgress節點來說,它是沒有current節點的,如果是在更新過程,由於current節點已經在首次渲染時產生了,所以workInProgress節點有對應的current節點存在。
最終會根據節點是首次渲染還是更新來決定是創建fiber還是diff fiber。只不過更新時,如果節點的優先級不夠會直接復用已有節點,即走跳出(bailout)的邏輯,而不是去走下面的更新邏輯。
節點可復用表示它無需更新。在上面beginWork的代碼中可以看到,若節點的優先級不滿足要求,說明它不用更新,會調用bailoutOnAlreadyFinishedWork
函數,去復用current節點作為新的workInProgress樹的節點。
beginWork函數中攔截無需更新節點的邏輯
if (!includesSomeLane(renderLanes, updateLanes)) {
...
// 此時無需更新,攔截無需更新的節點
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
beginWork它的返回值有兩種情況:
bailoutOnAlreadyFinishedWork
函數的返回值也是如此。
從這個函數中,我們也可以意識到,識別當前fiber節點的子樹有無更新顯得尤為重要,這可以決定是否終止當前Fiber子樹的遍歷,將復雜度直接降低。實際上可以通過fiber.childLanes去識別,childLanes如果不為空,表明子樹中有需要更新的節點,那麽需要繼續往下走。
標記fiber.childLanes的過程是在開始調度時發生的,在markUpdateLaneFromFiberToRoot 函數中
帶著上邊的認知,來看壹下源碼了解具體的復用過程:
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
workInProgress.dependencies = current.dependencies;
}
// 標記有跳過的更新
markSkippedUpdateLanes(workInProgress.lanes);
// 如果子節點沒有更新,返回null,終止遍歷
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
} else {
// 子節點有更新,那麽從current上復制子節點,並return出去
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
beginWork的主要功能就是處理當前遍歷到的fiber,經過壹番處理之後返回它的子fiber,壹個壹個地往外吐出fiber節點,那麽workInProgress樹也就會被壹點壹點地構建出來。
這是beginWork的大致流程,但實際上,核心更新的工作都是在各個更新函數中,這些函數會安排fiber節點依次進入兩大處理流程:計算新狀態和Diff算法,限於篇幅,這兩個內容會分兩篇文章詳細講解,可以持續關註。