Skip to main content

root render schedule の仕組み

root.render() の仕組みセクションでは、最終的にマイクロタスクを利用してアプリケーションのレンダーをスケジュールしました:

// Reactのコードはこのような形でした
scheduleImmediateTask(processRootScheduleInMicrotask);

// ほとんどの場合、実質的にこれと同様の処理になります
queueMicrotask(processRootScheduleInMicrotask);

より正確には、スケジュールされた root はグローバル変数firstScheduledRootpending状態のワークを保持しています。

ワークループとはコンポーネントのレンダリングと画面表示を行うプロセスです。このループは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();
  1. popstateイベント内であれば、root をSyncLaneに entangle
  2. scheduleTaskForRootDuringMicrotaskを呼び出して、この root に対するnextLanesを取得(ワークループがトリガーされる重要な関数)
  3. 保留中のワークが存在しない場合(nextLanes === NoLane)、root をチェーンから削除(リンクリスト形式の複数 root 管理)
  4. ワークが残っている場合、root をリストに保持し sync work を含むか確認
  5. すべての root で sync work をフラッシュ

How scheduleTaskForRootDuringMicrotask works

上記のステップ 2 で呼び出されるこの関数の定義:

シグネチャ

function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number
): Lane {
/* [Not Native Code] */
}

この関数はスケジュールされた最も優先度の高い lane(最小の lane)を返します。

実装ステップ

  1. 飢餓状態の 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;
    }

    実装はこのリンクで確認できます。

  2. 次の lane を計算

    次に React は次の lane を計算します:

    // root.render(children)から呼ばれる場合
    const nextLanes = getNextLanes(root, NoLanes);
  3. 保留中のワークが存在しない場合やデータでサスペンドしている場合に終了

    nextLanesNoLanes(保留中のワークが存在しない)の場合、またはルートがデータでサスペンドしている場合、またはコミットコールバックがスケジュールされている場合:

    • 既存のコールバックがあればキャンセル
    • root.callbackNoderoot.callbackPriorityプロパティをクリーンアップ
    • NoLaneを返す

    実際の実装はこちらで確認できます。

  4. Sync 保留中のワークが存在する場合に終了

    Sync ワークはprocessRootScheduleInMicrotaskによってフラッシュされ、次のセクションで詳細を説明します。

    So when a SyncLane, we do exactly same as the previous step, and return a SyncLane rather than NoLane.

  5. 現在の root にレンダーをスケジュール

    ここまでで、保留中の concurrent ワークが存在する場合、nextLanesから priority を計算します。しかし、どういうことでしょうか?

    新しいcallbackPriorityhighest 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については後述します。

    現時点ではscheduleCallbacksetTimeoutのようなものと考えてください。

    note

    リマインダー: すべてのスケジュールされた root をループ処理し、レンダーを再スケジュールした後、processRootScheduleInMicrotaskは最終的にすべての root で sync work をフラッシュします。

How flush sync work on roots works

この関数は、firstScheduledRootから始まるすべてのスケジュールされた root をループ処理し、非レガシー root やSyncLaneフラグを持たない root を除外します。

その後、performSyncWorkOnRootを呼び出し、syncワークループをトリガーします。

まずconcurrentワークループから説明し、その後syncワークループについて説明します。