ワークループの仕組み
ワークループの詳細に入る前に、最初にどのように開始されるかを見てみましょう。
concurrent ワークループの開始方法
concurrent root
の場合、最後に行った処理はscheduleTaskForRootDuringMicrotask
内でperformConcurrentWorkOnRoot
のコールバックをスケジュールすることです。
root に対する concurrent ワークの処理を見ていきましょう。
root.callbackNode = scheduleCallback(
schedulerPriorityLevel, // シンプルなroot.renderの場合NormalPriority
performConcurrentWorkOnRoot.bind(null, root)
);
performConcurrentWorkOnRoot
のシグネチャ
performConcurrentWorkOnRoot
は以下のように定義されています:
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean
): RenderTaskFn | null {
/* [Not Native Code] */
}
root
プロパティはコールバックスケジュール時にバインドされ、didTimeout
はスケジューラーがこのコールバックを実行する際に渡されます:
// スケジューラー内部(簡略化)
const didTimeout = didCallbackTimedOut();
performConcurrentWorkOnBoundRoot(didTimeout);
// ここで
const performConcurrentWorkOnBoundRoot = performConcurrentWork.bind(null, root);
この関数はコンポーネントのレンダリングと全体的なロジックを実行するため、非常に長くなりますが、WorkTags
、effects
タイプとその実装詳細、そしてもちろんhooks
など、一部の部分を個別のセクションに分割します。
実装詳細
TL;DR
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects && root.callbackNode !== originalCallbackNode) {
return null;
}
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
return null;
}
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (renderWasSuccessfull) {
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
} else {
// manage errors and suspense
}
ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
レンダリング中またはコミット中の呼び出しを防止
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error("Should not already be working.");
}パッシブエフェクトのフラッシュ
エフェクトについては別セクションで詳細に説明します。準備ができ次第リンクを追加予定です。
// 簡略化
// flushPassiveEffectsがアップデートをスケジュールする可能性があるため参照を保持
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects && root.callbackNode !== originalCallbackNode) {
return null;
}パッシブエフェクトがフラッシュされ、現在のスケジュールされたコールバックがキャンセルされた場合、この関数は実行を停止して
null
を返します。他のスケジュールが独自のワークをトリガーするためです。root.render()
の観点からは、現時点でエフェクトは存在しないため、処理を継続します。このセクションは現時点ではスキップ可能です。nextLanes
の再計算これは修正が必要な残存処理(現時点では)、next lanes はここで再計算されます(最初は
scheduleTaskForRootDuringMicrotask
関数内で計算済み)。当然ながら、
NoLanes
(処理すべきワークなし)の場合はnull
が返されます。タイムスライスするかどうかの判定
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);タイムスライスを使用するには、root が
BlockingLanes
またはExpiredLanes
を含んではいけません。Blocking lanes は(前出リンク参照):
SyncDefaultLanes
、InputContinuousHydrationLane
、InputContinuousLane
、DefaultHydrationLane
、DefaultLane
です。root.render()
を transition なしで実行した場合、DefaultLane
に属するため、タイムスライスなしのSync
レンダーとして扱われます。タイムスライスに基づくレンダー関数の呼び出し
タイムスライスするかどうかに応じて、
renderRootConcurrent
またはrenderRootSync
を呼び出します。let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);レンダリングロジック全体はこれらの関数呼び出し後に実行されます。
詳細は後述しますが、レンダー完了後も以下のステップを実行する点に注意してください。
レンダー完了後の処理と exitStatus に基づくロジックの実行
exitStatus の取り得る値はこちらで確認できます。レンダーが完了しなかった場合、React は root をsuspended 状態としてマークします。それ以外の場合は以下の処理が行われます:
外部ストアとの一貫性を検証 一貫性がない場合、同期レンダーを再実行します。
while(true)
ループは適切な exitStatus が得られるまでcontinue
で継続されます。RootErrored
エラー発生時、React は可能な限りエラーからの回復を試み、renderRootSync
を使った再レンダーを試行します。RootFatalErrored
の exitStatus は、React 自体のバグを示す可能性があります(詳細 TBD)上記以外の場合、ツリーは一貫しており作業完了とみなされます
// 簡略化
// Reactはレンダー作業の結果を各Fiberのalternateプロパティに保存
const finishedWork: Fiber = root.current.alternate;この変数は root の finishedWork プロパティに割り当てられ、
finishConcurrentRender
が呼び出されます。root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
Root の再スケジュールを保証
root.render()
の処理で見たように、マイクロタスクを再スケジュールして root のレンダーを試行します。ただし通常はnextLanes
を確認することで処理すべき作業がないことを検知します。Root の継続処理を返却
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
renderRootSync
の仕組み
ついに核心部分に到達しました!コンポーネントをレンダーする関数です。
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// [Not Native Code]
}
TL;DR
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher(root.containerInfo);
if (workInProgressRoot !== null || workInProgressRootRenderLanes !== lanes) {
// [...] some work
prepareFreshStack(root, lanes);
}
do {
try {
if (didSuspendDuringHydration) {
resetWIPStack();
workInProgressRootExitStatus = RootDidNotComplete;
break;
}
// [...] Other branches to break when needed
workLoopSync();
// Why a break here you wonder ? Hint: there is no break in the catch block
break;
} catch (e) {
handleThrow(root, e);
}
} while (true)
if (didSuspendInShell) {
root.shellSuspendCounter++;
}
executionContext = prevExecutionContext;
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
finishQueueingConcurrentUpdates();
return workInProgressRootExitStatus;
1. レンダー開始をマーク
WorkLoop モジュールが使用するグローバル変数executionContext
があります。これは Lanes と同じようにビットフラグで管理され、取り得る値は:
export const NoContext = /* */ 0b000;
const BatchedContext = /* */ 0b001;
export const RenderContext = /* */ 0b010;
export const CommitContext = /* */ 0b100;
レンダーが開始されると、executionContext
は以下のように更新されます:
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
// レンダー終了後、元の値に戻す:
executionContext = prevExecutionContext;
この変数は React にとって非常に重要で、現在実行中のワークの種類を判別し、誤った使用を防ぐ役割を果たします。
2. コンテキスト専用ディスパッチャーの設定
React ディスパッチャーは、hooks などのプロパティを持つオブジェクトで、依存性注入(dependency injection)の手法で振る舞いを伝播するために使用されます。
ワークループが開始されるこの時点では、root.render
パスから来る処理に対してContextOnlyDispatcher
が使用され、readContext
とuse
のみが許可されます。
React の新機能である cache についても同様の処理が行われ、専用のディスパッチャーが設定されます。ディスパッチャーの詳細については別セクションで解説します。
3. 新鮮なスタックの準備
次に、renderRoot
はスケジューリング時に引数から受け取った root とその lanes を、グローバル変数から取得した現在処理中の root と lanes と照合する必要があります。
if (workInProgressRoot !== null || workInProgressRootRenderLanes !== lanes) {
// some work
prepareFreshStack(root, lanes);
}
新鮮なスタックの準備とは、前回の作業中に追加された情報を削除し、クリアするプロセスです。
手順を見ていきましょう:
以下の
root
プロパティをnull
に設定finishedWork
finishedLanes
timeoutHandle
(コミット用の既存タイムアウトもキャンセル)cancelPendingCommit
work in progress スタックをリセット
root.render()
から来るケースでは何も行いませんが、いくつかの Fiber モジュールとそのグローバル変数(内部状態)をリセットし、進行中の作業を中断します。これはレンダーサイクルを開始する際にクリーンな状態が必要なため正常な動作です。このプロセスは最も重要かつクリティカルな処理です。work loop の内部状態変数をリセット
以下の変数をデフォルト値に設定します:
workInProgressRoot
=root
workInProgress
=createWorkInProgress(root.current, null)
: 2 つ目の fiber を作成するこの変数に注目renderLanes
=lanes
workInProgressRootRenderLanes
=lanes
workInProgressSuspendedReason
=NotSuspended
workInProgressThrownValue
=null
workInProgressRootDidAttachPingListener
=false
workInProgressRootExitStatus
=RootInProgress
workInProgressRootFatalError
=null
workInProgressRootSkippedLanes
=NoLanes
workInProgressRootInterleavedUpdatedLanes
=NoLanes
workInProgressRootRenderPhaseUpdatedLanes
=NoLanes
workInProgressRootPingedLanes
=NoLanes
workInProgressRootConcurrentErrors
=null
workInProgressRootRecoverableErrors
=null
createWorkInProgress
は非常に重要なので詳細を確認しましょう。これは FiberNode(root.current
)をミラーリングする 2 つ目の fiber(alternate
)を作成します。この処理は作業中に再度登場します。alternate は、進行中の作業(またはレンダー)の「下書き」や「スナップショット」として使用され、完了するとメインの fiber としてコミットされ、以前のメイン fiber は解放されます。
createWorkInProgress
は次のセクションで再び取り上げます。コンカレントアップデートのキューイングを完了
root.render()
の処理で見たconcurrentQueues
変数を覚えていますか?root.render()
は root 自体には何も変更を加えず、マイクロタスクキューを介してレンダーをスケジュールするだけで、レンダーするchildren
をアップデートとしてグローバル変数に保存していました:concurrentQueues[id++] = fiber;
concurrentQueues[id++] = sharedQueue;
concurrentQueues[id++] = update;
concurrentQueues[id++] = lane;ここでこの配列を処理します:
配列の終端までループし、同じ順序で変数を参照しながら配列要素を
null
設定で削除while (i < end) {
const fiber: Fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
const queue: ConcurrentQueue = concurrentQueues[i];
concurrentQueues[i++] = null;
const update: ConcurrentUpdate = concurrentQueues[i];
concurrentQueues[i++] = null;
const lane: Lane = concurrentQueues[i];
concurrentQueues[i++] = null;
// この時点でアップデートはfiberのキューにアタッチされる
if (queue !== null && update !== null) {
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
if (lane !== NoLane) {
// fiberからrootまでlanesをマージ(alternateが存在する場合も含む)
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
4. ワークループのトリガー
ついに本当のコア部分に到達しました(ほぼ!)ここからが本番です!
React はdo while(true)
ループを開始し、workLoopSync()
(本物のワークループ)を呼び出します。簡略化したループを見てみましょう:
// 簡略化
do {
try {
if (didSuspendDuringHydration) {
resetWIPStack();
workInProgressRootExitStatus = RootDidNotComplete;
break;
}
workLoopSync();
// ここにbreakがある理由は?ヒント: catchブロックにはbreakがない
break;
} catch (e) {
handleThrow(root, e);
}
} while (true);
workLoopSync
は次のように定義されています:
function workLoopSync() {
// Reactコードベースのコメントをよく読んでください
// Fiber間でyieldが必要かどうかをチェックせずに作業を実行
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// Reactコードベースのコメントをよく読んでください
// Schedulerがyieldを要求するまで作業を実行
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
の動作を簡略化して説明します:
// simplified
// the unitOfWork Fiber passed here is the global workInProgress variable
// which was initialized by the fiberRoot.current.alternate
// so basically the workInProgress initially = the alternate
// which means that the 'current' tree is the workInProfress.alternate
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
つまり、beginWork は fiber をレンダーし、次の fiber を返します。
beginWork
の詳細は次のセクションで説明しますが、この段階で重要なのは、現在の作業単位とその alternate の両方を受け取ることです。これはrenderLanes
を使用してメインのfiber
をalternate
にレンダーし、次のunit of work
を返すようなものです。
前述のように、alternate
はworkInProgress
を指し、元のfiber
はcurrent
ツリーとなります。
これが実際の同期ワークループです: やるべき作業がなくなるまで作業を続行します
すでにご存知かもしれませんが、React はroot.render
に渡されたchildren
から fiber ツリーを構築し、最後に到達するまで作業を続けます。それらがどのようにリンクされ、次の fiber がどのように選択されるかは別のセクションで詳述します。ここではループの説明に焦点を当てます。
beginWork
は最終的にreconcileChildren
関数を呼び出し、次に処理する子要素を見つけます。
React はツリーを下方向に移動し、最下部に到達するまで各コンポーネントとその最初の子要素を再帰的に処理します。
次の子要素がnull
の場合、React は子を持たない作業に対してcompleteWork
を呼び出します。このプロセスでは、React は兄弟要素(ツリーの同じレベルのコンポーネント)または親の兄弟要素(叔父要素)を処理します。
このプロセスの詳細は「completeWork の仕組み」セクションで説明します。
5. スローされたエラーの処理
レンダーフェーズでエラーがスローされた場合、React は Suspense フォールバックを表示するか、エラーバウンダリを表示するか、空白ページを表示するかのいずれかになります。
このプロセスは複雑で長く、多くのケースを管理します。これについては別途セクションを設けて説明します。
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
6. 実行コンテキストとディスパッチャーの復元
ここまででレンダーが完了すると、React はいくつかの変数をリセットし、チェックを実行します:
// workloop code
resetContextDependencies();
restoreExecutionContextAndDispatcher();
throwAsDefensiveIfThereIsStillSomeWorkToDo();
Reset context dependencies
これはコンテキストモジュールの内部状態(モジュール変数)をリセットします。レンダーが完了したためです。
Restore execution context
// 前述の通り
executionContext = prevExecutionContext;Throw if there is still work to do
if (workInProgress !== null) {
throw new Error(
"Cannot commit an incomplete root. This error is likely caused by a " +
"bug in React. Please file an issue."
);
}
7. アップデートの完了
これは新鮮なスタックを準備した時と同じプロセスで、React はレンダリング中に別のアップデートがスケジュールされた場合に備えてconcurrentQueues
に対して 2 回目のパスを実行します。
シンプルなケースでは何も行われません。
finishQueueingConcurrentUpdates();
renderRootConcurrent
の仕組み
sync
レンダーとの顕著な違いがあります。root.render()
からこのパスを通るには、shouldTimeSlice
条件を満たす必要があります。
これを行う最も簡単な方法(唯一の方法ではないにしても)は、root.render()
をstartTransition
でラップすることです。
この場合、renderRootConcurrent
は同じシグネチャで呼び出されます。詳細を見てみましょう:
todo: begin work と leaf セクションの後に戻る