beginWork の仕組み
beginWork は React アプリケーションをレンダーする関数です。
前のセクションで次のように呼び出されました:
// 簡略化
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
// current: オリジナルのfiber(画面上に表示されているもの)
// unitOfWork: alternate fiber(現在進行中のレンダー)
// renderLanes: prepareFreshStackでグローバルに割り当て済み
const next = beginWork(current, unitOfWork, renderLanes);
// ... 残りのコード
}
シグネチャ
beginWork
は次のように定義されています:
function beginWork(
current: Fiber | null, // 現在のfiberツリー
workInProgress: Fiber, // alternate
renderLanes: Lanes
): Fiber | null {
/* [Not Native Code] */
}
次の作業単位を返します。その計算方法を見ていきます。次の作業単位は、次の current fiber の alternate となります。
実装
React コンポーネントはライフタイム中に複数回レンダーされます。alternate
は各レンダーごとに作成され、コンポーネント出力の次バージョンの下書きとして機能します。
beginWork
の簡略化バージョンは次のようになります:
function beginWork(current, wip, lanes) {
if (current !== null) {
// コンポーネントがアップデート中
} else {
// コンポーネントの初回レンダー
}
}
可能な場合の早期 bailout 試行
beginWork
は最初に再レンダーかどうかをチェックします。root.render()
から来るケースでは該当しませんが、とにかくこのコードパスはすべてのレンダーで通過します。
root.render
から来る場合でも、HostRoot
fiber は新鮮なスタック準備時に作成された alternate を持ちます。ただしツリーの残りの部分は持ちません。
alternate が既に存在する場合:
oldProps
(current.memoizedProps
)とnewProps
(alternate(wip).pendingProps
)を参照:const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;oldProps
とnewProps
が異なる場合(親コンポーネントからのレンダートリガー)またはlegacy context が変更された場合、コンポーネントがアップデートを受信したとマークします。props が変更された場合、スケジュールされたアップデートまたはコンテキストの変更があるかチェック:
- コンポーネントがアップデートを受信したとは、alternate の
lanes
がrenderLanes
と交差することを意味します(ここで少し考えてみましょう)。 - コンテキストは
fiber.dependencies
にリンクリストとして保存されるため、すべてを反復処理してコンテキストの値を比較します。
何も変更がない場合、React は可能であればこのコンポーネントとその子のレンダーをbailoutしようと試みます。bailout については専用セクションで説明します。
- コンポーネントがアップデートを受信したとは、alternate の
current
がnull
の場合(コンポーネントの初回レンダー)、コンポーネントがアップデートを受信していないとマークし、ハイドレーション関連の処理を実行します。これは現在のスコープ外です。
これまでの説明をコードに落とし込みます:
// 簡略化
function beginWork(
current: Fiber | null, // 存在する場合の描画済みfiber(HostRootでは保証)
workInProgress: Fiber, // 保留中のレンダーfiber
renderLanes: Lanes
) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceivedUpdate = true;
} else {
if (
hasScheduledUpdateOrContext(current, renderLanes) &&
// 詳細は後述
workInProgress.flags & (DidCapture === NoFlags)
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes
);
}
}
} else {
didReceiveUpdate = false;
// スコープ外のコード
}
}
要約すると、beginWork
は不要な作業を bailout しようと試みます。これは React で唯一の bailout ではなく、何度も登場します。
コンポーネントのレンダー
次に、React はworkInProgress.tag
に対して巨大なswitch 文を実行します。
この switch 文の目的は、現在の fiber をレンダーする適切な関数にリダイレクトすることです。
workTag
の詳細は次のセクションで説明しますが、ここではroot.render()
から来るケースの表面をなぞる程度に留めます。次のセクションでは、各 workTag のレンダーを説明する前に、workTag の仕組みについて深く掘り下げます。
// 簡略化
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
// 前のコード
switch (workInProgress.tag) {
// case FunctionComponent:
// case ClassComponent:
// case IndeterminateComponent:
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// case HostPortal:
// case HostComponent:
// case HostText:
// case Fragment:
// case Mode:
// case ContextConsumer:
// case ContextProvider:
// case ForwardRef:
// case Profiler:
// case SuspenseComponent:
// case MemoComponent:
// case SimpleMemoComponent:
// case LazyComponent:
// case IncompleteClassComponent:
// case DehydratedFragment:
// case SuspenseListComponent:
// case ScopeComponent:
// case OffscreenComponent:
// case LegacyHiddenComponent:
// case CacheComponent:
// case TracingMarkerComponent:
// case HostHoistable:
// case HostSingleton:
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
"React. Please file an issue."
);
}
ご覧の通り、サポートされているすべてのタグに対して case が存在します。
HostRoot
fiber の初回レンダーでは、updateHostRoot(current, workInProgress, renderLanes)
を返します。
updateHostRoot
も簡単に見てみましょう。
PS: これは root の初回レンダーを説明しています。
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes
) {
// 現時点では無視する
pushHostRootContext(workInProgress);
// 防御的ガード: root fiberは常にcurrentとalternateを持つ
if (current === null) {
throw new Error("Should have a current fiber. This is a bug in React.");
}
const nextProps = workInProgress.pendingProps; // nextPropsはnull
const prevState = workInProgress.memoizedState; // { element: null }
const prevChildren = prevState.element; // prevChildren = null
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// ここで一旦停止。次のコードはupdateQueueの処理が完了している前提でプロパティを使用するため
// cloneとキューの処理を説明した後に続けます
}
updateQueue
のクローン作成
updateQueue
はcreateRoot
の仕組みで定義され、root.render
の処理中に pending shared queue が設定されました。
初回レンダーおよびアプリ実行中にこのパスに到達すると仮定すると、この関数は、current
fiber と同じ queue を持つ場合、新しいクローンupdateQueue
をalternate
にアタッチします。
function cloneUpdateQueue<State>(current: Fiber, workInProgress: Fiber): void {
// 現在のupdateQueueからクローンを作成(既にクローンでない場合)
const queue: UpdateQueue<State> = workInProgress.updateQueue;
const currentQueue: UpdateQueue<State> = current.updateQueue;
if (queue === currentQueue) {
const clone: UpdateQueue<State> = {
baseState: currentQueue.baseState,
firstBaseUpdate: currentQueue.firstBaseUpdate,
lastBaseUpdate: currentQueue.lastBaseUpdate,
shared: currentQueue.shared, // shared.pendingが重要
callbacks: null,
};
workInProgress.updateQueue = clone;
}
}
updateQueue の処理
このプロセスは長く非常に複雑なため、現段階では簡略化して説明します。詳細は後述します。
このクローン作成と処理パスはHostRoot
、ClassComponent
、および実験的なCacheComponent
で実行可能です。
pending queue は循環構造を持ち、最大 2 つのエントリ(最新のものが最後)を含みます。これは切断され、while(true)
ループで処理されます。
このセクションは付録に移動します。非常に複雑で説明に時間がかかるためです。
レンダーフェーズを続行するための簡略化バージョン:
// root.render()から来る場合、循環的に1つのアップデートのみ存在
prepareTheOrderOfUpdatesToProcess();
let update = firstBaseUpdate;
let newState = queue.baseState;
do {
const queue = workInProgress.updateQueue;
newState = getStateFromUpdate(wip, queue, update, newState, props);
if (update.callback) {
queue.callbacks.push(update.callback);
}
update = update.next;
// pending updateなし
if (update === null) {
if (queue.shared.pending === null) {
break;
} else {
update = appendPendingUpdates();
}
}
} while (true);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
getStateFromUpdate
は
update.tag
をスイッチングし(現時点では state を更新)、最終的にroot.render()
に渡したchildren
を含む{ element }
状態を生成します。
次にupdateHostRoot
に戻りましょう:
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes
) {
// 現時点では無視する
pushHostRootContext(workInProgress);
// 防御的ガード: root fiberは常にcurrentとalternateを持つ
if (current === null) {
throw new Error("Should have a current fiber. This is a bug in React.");
}
const nextProps = workInProgress.pendingProps; // nextPropsはここではnull
const prevState = workInProgress.memoizedState; // { element: null }
const prevChildren = prevState.element; // prevChildren = null
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// ここで一旦停止。次のコードはupdateQueueの処理が完了している前提でプロパティを使用するため
// nextState = {element: children, isDehydrated: false, cache: {...} }
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
bailoutOnAlreadyFinishedWork
とreconcileChildren
はレンダー処理中に多くの場所から呼び出される重要な関数で、専用の章で説明する価値があります。
初回レンダーではbailoutOnAlreadyFinishedWork
を通りませんが、reconcileChildren
がここでの鍵となります!簡易説明:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
// fiberの初期マウント時(HostRootはここを通らない)
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress, // returnFiber(子の親)
current.child, // 現在描画されている最初の子
nextChildren, // 新しい最初の子
renderLanes // このレンダーで使用されるlane(root.renderのDefaultLane)
);
}
}
HostRoot
fiber を処理する場合、current は常に存在するため、reconcileChildFibers
はreconcileChildFibers
を呼び出し、thenable カウンターをリセットした後、同じ引数でreconcileChildFibersImpl
を呼び出します。
reconcileChildFibersImpl
の仕組み
この関数はchildren
のレンダリングを担当します。前節で見たように、children はarray
要素やstring
など様々な形式を取り得ます。
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
// [Not Native Code]
}
この関数の動作を段階的に見ていきましょう:
キーなしトップレベル Fragment のスキップ
React はまず、トップレベルの子要素がキーなしの
Fragment
かどうかを確認します。該当する場合、そのFragment
をスキップします// キーなしトップレベルFragmentの判定方法:
const isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}オブジェクト型の children に対する処理
children が non-null オブジェクトの場合、$$typeof
プロパティで分岐処理します。Dan のこのプロパティに関する解説が参考になります。if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
}
case REACT_PORTAL_TYPE: {
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
}
case REACT_LAZY_TYPE: {
// 今は無視
}
}
}
if (isArray(newChild)) {
// 後ほど説明
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if (getIteratorFn(newChild)) {
// 後ほど説明
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}Promise 型 children の処理
children が.then
メソッドを持つオブジェクト(非同期コンポーネント)の場合、Promise をアンラップして結果を再処理します:if (typeof newChild.then === "function") {
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(newChild),
lanes
);
}danger非同期コンポーネントは実験的機能で、クライアント側では十分にサポートされていません。新しい Promise が生成されるたびに以前のツリーが削除されるため、キャッシュ戦略なしで使用すべきではありません(元々リクエストごとに 1 回呼び出されるサーバー向けに設計されました)。
気付いたかもしれませんが、reconciliation は実際のレンダーの前段階で実行されます。
reconciliation の目的は、現在のツリーから作成した alternate に次のツリーを移植することです。
root.render()
から来るこの段階では、子要素用の Fiber すら作成されていないため、最初のステップで Fiber を作成することになります。
reconcileSingleElement
の仕組み
これは実際には reconciliation プロセスの一部であり、専用のセクションで説明されます。
この関数が最初に行うことは、currentFirstChild
からkey
やtype
が変更されたかどうかを確認することです。変更があった場合、子をparentFiber
のdeletions
プロパティに追加することで削除を追跡します。これにより、commit phase でクリーンアップ effect を実行できるようになります。
次に、この関数はアプリケーション用の新しい fiber を作成して返します:
const created = createFiberFromElement(element, returnFiber.mode, lanes);
root.render()
から来るケースでは、この関数で作成される最初の fiber は以下の 3 つ目になります:
- fiber root にアタッチされた current fiber
- その alternate
root.render()
に渡した最初の子用の fiber(少なくとも 3 つ目)
現時点で、2 つの主要な未説明セクションが残っています:
beginWork
関数内の大きなswitch-case
(how rendering works
セクションで説明)reconcileSingleElement
の動作と fiber の作成方法(the reconciliation works
セクションで説明)
このセクションが過度に長くなるのを避けるため、これらは次のセクションに移動します。
Recap
beginWork
はperformUnitOfWork(workInProgress)
内で呼び出され、次にレンダリングするツリーの子要素を reconcile する役割を担います。
// renderSync内で簡略化
while (unitOfWork !== null) {
performUnitOfWork(unitOfWork);
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceivedUpdate = true;
} else {
if (
hasScheduledUpdateOrContext(current, renderLanes) &&
// 詳細は後述
workInProgress.flags & (DidCapture === NoFlags)
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes
);
}
}
} else {
didReceiveUpdate = false;
// スコープ外のコード
}
switch (workInProgress.tag) {
// case FunctionComponent:
// case ClassComponent:
// case IndeterminateComponent:
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// case HostPortal:
// case HostComponent:
// case HostText:
// case Fragment:
// case Mode:
// case ContextConsumer:
// case ContextProvider:
// case ForwardRef:
// case Profiler:
// case SuspenseComponent:
// case MemoComponent:
// case SimpleMemoComponent:
// case LazyComponent:
// case IncompleteClassComponent:
// case DehydratedFragment:
// case SuspenseListComponent:
// case ScopeComponent:
// case OffscreenComponent:
// case LegacyHiddenComponent:
// case CacheComponent:
// case TracingMarkerComponent:
// case HostHoistable:
// case HostSingleton:
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
"React. Please file an issue."
);
}
updateHostRoot
はトップレベルの root オブジェクトの updateQueue を処理し、新しいツリーをレンダリングする結果をもたらします。これについては次のセクションで説明します。