Skip to main content

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()から来るケースでは該当しませんが、とにかくこのコードパスはすべてのレンダーで通過します。

note

root.renderから来る場合でも、HostRoot fiber は新鮮なスタック準備時に作成された alternate を持ちます。ただしツリーの残りの部分は持ちません。

alternate が既に存在する場合:

  • oldPropscurrent.memoizedProps)とnewPropsalternate(wip).pendingProps)を参照:

    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
  • oldPropsnewPropsが異なる場合(親コンポーネントからのレンダートリガー)またはlegacy context が変更された場合、コンポーネントがアップデートを受信したとマークします。

  • props が変更された場合、スケジュールされたアップデートまたはコンテキストの変更があるかチェック:

    • コンポーネントがアップデートを受信したとは、alternate のlanesrenderLanesと交差することを意味します(ここで少し考えてみましょう)。
    • コンテキストはfiber.dependenciesにリンクリストとして保存されるため、すべてを反復処理してコンテキストの値を比較します。

    何も変更がない場合、React は可能であればこのコンポーネントとその子のレンダーをbailoutしようと試みます。bailout については専用セクションで説明します。

  • currentnullの場合(コンポーネントの初回レンダー)、コンポーネントがアップデートを受信していないとマークし、ハイドレーション関連の処理を実行します。これは現在のスコープ外です。

これまでの説明をコードに落とし込みます:

// 簡略化
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;
// スコープ外のコード
}
}
note

要約すると、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のクローン作成

updateQueuecreateRootの仕組みで定義され、root.renderの処理中に pending shared queue が設定されました。

初回レンダーおよびアプリ実行中にこのパスに到達すると仮定すると、この関数は、current fiber と同じ queue を持つ場合、新しいクローンupdateQueuealternateにアタッチします。

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 の処理

このプロセスは長く非常に複雑なため、現段階では簡略化して説明します。詳細は後述します。

このクローン作成と処理パスはHostRootClassComponent、および実験的な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;

getStateFromUpdateupdate.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;
}

bailoutOnAlreadyFinishedWorkreconcileChildrenはレンダー処理中に多くの場所から呼び出される重要な関数で、専用の章で説明する価値があります。

初回レンダーでは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 は常に存在するため、reconcileChildFibersreconcileChildFibersを呼び出し、thenable カウンターをリセットした後、同じ引数でreconcileChildFibersImplを呼び出します。

reconcileChildFibersImplの仕組み

この関数はchildrenのレンダリングを担当します。前節で見たように、children はarray要素やstringなど様々な形式を取り得ます。

function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
// [Not Native Code]
}

この関数の動作を段階的に見ていきましょう:

  1. キーなしトップレベル 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;
    }
  2. オブジェクト型の 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
    );
    }

    reconcileChildrenArrayreconcileChildrenIteratorは後述します。

  3. Promise 型 children の処理
    children が.thenメソッドを持つオブジェクト(非同期コンポーネント)の場合、Promise をアンラップして結果を再処理します:

    if (typeof newChild.then === "function") {
    return reconcileChildFibersImpl(
    returnFiber,
    currentFirstChild,
    unwrapThenable(newChild),
    lanes
    );
    }
    danger

    非同期コンポーネントは実験的機能で、クライアント側では十分にサポートされていません。新しい Promise が生成されるたびに以前のツリーが削除されるため、キャッシュ戦略なしで使用すべきではありません(元々リクエストごとに 1 回呼び出されるサーバー向けに設計されました)。

tip

気付いたかもしれませんが、reconciliation は実際のレンダーの前段階で実行されます。

reconciliation の目的は、現在のツリーから作成した alternate に次のツリーを移植することです。

root.render()から来るこの段階では、子要素用の Fiber すら作成されていないため、最初のステップで Fiber を作成することになります。

reconcileSingleElementの仕組み

これは実際には reconciliation プロセスの一部であり、専用のセクションで説明されます。

この関数が最初に行うことは、currentFirstChildからkeytypeが変更されたかどうかを確認することです。変更があった場合、子をparentFiberdeletionsプロパティに追加することで削除を追跡します。これにより、commit phase でクリーンアップ effect を実行できるようになります。

次に、この関数はアプリケーション用の新しい fiber を作成して返します:

const created = createFiberFromElement(element, returnFiber.mode, lanes);

root.render()から来るケースでは、この関数で作成される最初の fiber は以下の 3 つ目になります:

  1. fiber root にアタッチされた current fiber
  2. その alternate
  3. root.render()に渡した最初の子用の fiber(少なくとも 3 つ目)
info

現時点で、2 つの主要な未説明セクションが残っています:

  1. beginWork関数内の大きなswitch-casehow rendering worksセクションで説明)
  2. reconcileSingleElementの動作と fiber の作成方法(the reconciliation worksセクションで説明)

このセクションが過度に長くなるのを避けるため、これらは次のセクションに移動します。

Recap

beginWorkperformUnitOfWork(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 を処理し、新しいツリーをレンダリングする結果をもたらします。これについては次のセクションで説明します。