root render schedule の仕組み
root.render() の仕組み
セクションでは、最終的にマイクロタスクを利用してアプリケーションのレンダーをスケジュールしました:
// Reactのコードはこのような形でした
scheduleImmediateTask(processRootScheduleInMicrotask);
// ほとんどの場合、実質的にこれと同様の処理になります
queueMicrotask(processRootScheduleInMicrotask);
より正確には、スケジュールされた root はグローバル変数firstScheduledRoot
にpending
状態のワークを保持しています。
ワークループとはコンポーネントのレンダリングと画面表示を行うプロセスです。このループはroot.render()
、コンポーネントの更新、Suspense からの回復など、様々なアクションによってトリガーされます。
root.render()
から説明を始めたので、まずワークループに到達するまでの流れを説明し、その後ループの詳細に踏み込みます。
processRootScheduleInMicrotask
の仕組み
コールスタックが空になると、JavaScript のイベントループはタスクキューを処理し、ここでスケジュールされたコールバックを最終的に実行します。
複数の root が存在し得る理由は、React がサーバー側でも動作し、複数のリクエストを並行してレンダリングできるため、あるいはクライアント側で手動で行う場合もあるからです。
React はコンカレント機能を実現するため、グローバル変数を巧みに(そして危険なほど)操作します。これらのグローバル変数は、モジュールのコンパクトな内部状態を表現しており、外部からの操作は専用の関数を通じて行われることがほとんどです。
processRootScheduleInMicrotaskはスケジュールされた root をループ処理します(シンプルなケースであるroot.render()
による小さなアプリケーションのレンダリングでは、1 つの root のみが存在します)。各 root に対して:
let root = firstScheduledRoot;
while (root !== null) {
// 現在のrootに対するロジックを実行
root = next;
}
要約すると、処理の流れは以下のように簡略化できます:
// 簡略化したコード
const currentTime = Date.now();
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
// 1
entangleSyncLaneIfInsidePopStateEvent(root);
// 2
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
// 3
if (nextLanes === NoLane) {
// 保留中のワークが存在しない
detachRootFromScheduledRoots(root);
} else {
// 4
if (includesSyncLane(nextLanes)) {
mightHavePendingSyncWork = true;
}
}
root = next;
}
// 5
flushSyncWorkOnAllRoots();
popstate
イベント内であれば、root をSyncLane
に entanglescheduleTaskForRootDuringMicrotask
を呼び出して、この root に対するnextLanes
を取得(ワークループがトリガーされる重要な関数)- 保留中のワークが存在しない場合(
nextLanes === NoLane
)、root をチェーンから削除(リンクリスト形式の複数 root 管理) - ワークが残っている場合、root をリストに保持し sync work を含むか確認
- すべての root で sync work をフラッシュ
How scheduleTaskForRootDuringMicrotask
works
上記のステップ 2 で呼び出されるこの関数の定義:
シグネチャ
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number
): Lane {
/* [Not Native Code] */
}
この関数はスケジュールされた最も優先度の高い lane(最小の lane)を返します。
実装ステップ
飢餓状態の lane を expired としてマーク
この処理では、root の
pendingLanes
を 1 つずつ走査し、expiration time が計算されていない場合は計算を行い、既に期限切れの場合はroot.expiredLanes
に追加します:// simplified
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
// 以下の2行で、laneから最高優先度のindexを取得し、
// そのindex分だけ1をシフトして単一の有効ビット(2の累乗)を持つ現在のlaneを取得
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
// このlaneのexpiration time
const expirationTime = root.expirationTimes[index];
// まだスケジュールされていない状態
if (expirationTime !== NoTimestamp) {
// このlaneがサスペンドされていない、またはpingedされている状態
// pinged lanesは、サスペンドしていたルートがプロミスの解決時に再開されたlaneを指します
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// 期限切れでない場合
root.expiredLanes |= lane;
}
lanes &= ~lane;
}実装はこのリンクで確認できます。
次の lane を計算
次に React は次の lane を計算します:
// root.render(children)から呼ばれる場合
const nextLanes = getNextLanes(root, NoLanes);保留中のワークが存在しない場合やデータでサスペンドしている場合に終了
nextLanes
がNoLanes
(保留中のワークが存在しない)の場合、またはルートがデータでサスペンドしている場合、またはコミットコールバックがスケジュールされている場合:- 既存のコールバックがあればキャンセル
root.callbackNode
とroot.callbackPriority
プロパティをクリーンアップNoLane
を返す
実際の実装はこちらで確認できます。
Sync 保留中のワークが存在する場合に終了
Sync ワークは
processRootScheduleInMicrotask
によってフラッシュされ、次のセクションで詳細を説明します。So when a
SyncLane
, we do exactly same as the previous step, and return aSyncLane
rather thanNoLane
.現在の root にレンダーをスケジュール
ここまでで、保留中の concurrent ワークが存在する場合、
nextLanes
から priority を計算します。しかし、どういうことでしょうか?新しい
callbackPriority
はhighest priority lane
として実装され、既存のcallback priorityと比較されます。priority が変更されていない場合、React は同じタスクと priority を再利用します。変更がある場合、
highest priority lane
からpriority
を推測する必要があります。highest priority lane
とはnextLanes
の中で最小の lane を指します。現在までに4 つの priorityが定義されています:
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLne;
export const IdleEventPriority: EventPriority = IdleLane;なぜ
Lane
の値を割り当てるのでしょうか?priority は lane のブレークポイントのようなもので、すべての lane を 4 つの priority グループに分類できます。実際の実装は以下の通りです:
// simplified
const lane = getHighestPriorityLane(nextLanes);
if (DiscreteEventPriority > lane) {
return DiscreteEventPriority;
}
if (ContinuousEventPriority > lane) {
return ContinuousEventPriority;
}
if (DefaultEventPriority > lane) {
return DefaultEventPriority;
}
return IdleEventPriority;次に、EventPriority をSchedulerPriorityに変換します:
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}この部分はこちらで実装されています。
最後に、ワークのスケジューリングを行います:
const newCallbackNode = scheduleCallback(
// シンプルなroot.renderの場合NormalPriority
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;scheduleCallback
についてはschedulerの仕組み
セクションで詳述されているため、ここでは説明しません。performConcurrentWorkOnRoot
については後述します。現時点では
scheduleCallback
をsetTimeout
のようなものと考えてください。noteリマインダー: すべてのスケジュールされた root をループ処理し、レンダーを再スケジュールした後、
processRootScheduleInMicrotask
は最終的にすべての root で sync work をフラッシュします。
How flush sync work on roots works
この関数は、firstScheduledRoot
から始まるすべてのスケジュールされた root をループ処理し、非レガシー root やSyncLane
フラグを持たない root を除外します。
その後、performSyncWorkOnRoot
を呼び出し、sync
ワークループをトリガーします。
まずconcurrent
ワークループから説明し、その後sync
ワークループについて説明します。