root.render()
の動作原理
React を使用して UI をレンダーする際の最初のステップは以下の通りです:
createRoot
で root オブジェクトを作成root.render(ui)
関数を呼び出し
import { App } from "./app";
import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
// これが最初のステップ
const root = createRoot(container);
// 次に2番目のステップ
root.render(<App />);
本セクションではroot.render
関数(2 番目のステップ)に焦点を当て、そのシグネチャと内部動作を解説します。
定義
宣言
Fiber root のrender
メソッドはこちらで宣言されています。
シグネチャ
render
メソッドは以下のように定義されます:
function render(children: ReactNodeList): void {
// [Not Native Code]
}
私たちがこのパラメータをapp
やui
と呼びがちなのとは対照的に、React のコード内ではchildren
として参照されます。ここではchildren
という呼称を使用しましょう 😉
このパラメータの型はReactNodeList
で、定義はこちらにあります:
type ReactNodeList = ReactEmpty | React$Node;
// where:
type ReactEmpty = null | void | boolean;
// and
type React$Node =
| null
| boolean
| number
| string
| React$Element<any>
| React$Portal
| Iterable<React$Node>;
// where
type React$Element<ElementType extends React$ElementType> = {
ref: any;
type: ElementType;
key: React$Key | null;
props: React$ElementProps<ElementType>;
};
上記の定義から、render
メソッドには以下のような様々な要素を渡すことが可能です(サンプルコードや実際のアプリケーションで使用される複雑な構造を含む):
- index.html
- index.js
<body>
<div id="root1"></div>
<hr />
<div id="root2"></div>
<hr />
<div id="root3"></div>
</body>
import React, { createElement } from "react";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root1")).render([
"Hello ",
<span key="world" style={{ color: "red" }}>
World!
</span>,
]);
class ClassComponent extends React.Component {
render() {
const { initialCount } = this.props;
return <p>Class Count is: {initialCount}</p>;
}
}
createRoot(document.getElementById("root2")).render([
<ul key="list">
<li>First item</li>
<li>Second</li>
<li>Last, not third</li>
</ul>,
createElement(
function FunctionComponent({ initialCount }) {
return <span>Function Count is: {initialCount}</span>;
},
{ initialCount: 2, key: "count" }
),
<ClassComponent key="class" initialCount={3} />,
]);
createRoot(document.getElementById("root3")).render([
null,
true,
false,
undefined,
]);
要約すると、React Element またはそのコレクションを渡すことになります。React はこれらを再帰的にレンダーし、インタラクティブな UI を表示します。
実装
上記の実装リンクをクリックした場合にお気づきかもしれませんが、render
メソッドは以下のようになっています:
// simplified
ReactDOMRoot.prototype.render = function render(children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error("Cannot update an unmounted root.");
}
// __DEV__ only checks
updateContainer(children, root, null, null);
};
この関数は人間が理解しやすい言葉で説明すると以下の処理を行います:
root._internalRoot
(FiberRootNode)が null の場合(root.unmount
が呼び出されたか手動で行われた場合)にエラーをスロー__DEV__
環境向けのチェックと警告を実行:- レガシーな
ReactDOM.render(children, callback)
のように第 2 引数に関数を渡した場合 - 第 2 引数に children を渡した場合(レガシーなシグネチャ使用と判断)
- 第 2 引数に何かしら値を渡した場合
- レガシーな
updateContainer(children, root, null, null)
を呼び出し
updateContainer
updateContainer
は React コードベースの多くの箇所から呼び出される関数で、なぜ「レンダー」や「マウント」ではなく「アップデート」という名前なのか疑問に思うかもしれません。これは React が常にツリーを更新中であると扱うためです。React はツリーのどの部分が初回マウントであるかを認識し、必要に応じて適切なコードを実行します。このシリーズの後半で詳しく説明します。
この関数の実装を分析することが重要です:
シグネチャ
export function updateContainer(
element: ReactNodeList, // children
container: OpaqueRoot, // OpaqueRoot = FiberRoot = new FiberRootNode
parentComponent?: React$Component<any, any>,
callback?: Function
): Lane {
// [Not Native Code]
}
この関数は多くの処理を実行し、ツリーの初回マウント時とその後のアップデート時に使用されます。
最後の 2 つのパラメータはroot.render
から呼び出される際にnull
として渡されます。これらは使用されないことを意味します。必要な場合にのみ説明します。
ここからはupdateContainer
のステップを簡略化したバージョンで説明していきます。
const current = container.current;
const lane = requestUpdateLane(current);
const update = createUpdate(lane);
update.payload = {element};
update.callback = callback;
const root = enqueueUpdate(current, update, lane);
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
1. 現在のFiber
への参照
この関数に渡されるcontainer
は、createRoot
に渡した DOM 要素ではありません。
これはroot._internalRoot
(FiberRootNode 型)です。
前章で学んだように、container.current
プロパティはFiberNode
型です。これがアプリケーションで作成された最初の Fiber です。
React はこの Fiber を参照するため、current
はfiber
またはfiberNode
を意味します。
const current = container.current;
2. アップデート用レーンの要求
次に React は、現在の Fiber に対するアップデート用レーン(数値)を要求します:
const lane = requestUpdateLane(current);
これが私たちが初めて遭遇する本物の「Lanes」です。理解を容易にするため、ビット演算と 2 進数表現についての知識が必要です。
Lane
は 2 の累乗数(1, 2, 4, 8, 16, 32...)で、2 進数表現で 1 つの有効ビット(1)のみを持つ整数です。React のコードベースではここで定義されています。
主なレーンとして以下が使用されます:
SyncLane
InputContinuousLane
IdleLane
OffscreenLane
など
// Reactのコードベースより
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
異なるレーンを組み合わせることで、新しい整数値(最大 32 状態)を生成します。適切なビットマスクを使用することで、複数のレーンを 1 つの数値に結合し、React が機能や挙動を検出・制御できるようにします。
React における複数レーンの組み合わせを「Lanes」と呼びます。
// Reactのコードベースより
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
// 個人コメント:これはLanesであるべき? 不明
export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
The requestUpdateLane
は、fiber.mode
(FiberNode
から取得)や他の変数を使用して必要なアップデートレーンを推論します。この関数は初期レンダー後にも実行時に呼び出されるため、そのまま説明します:
モードが Concurrent でない場合(
(mode & ConcurrentMode) === NoMode
)、SyncLane
(2
)が返されますレンダーフェーズアップデート(レンダー中に
setState
を呼び出す)の場合、最優先レーンが返されます:技術的には
lane & -lane
で、数値 n に対してn & -n = 2^k
(k は n の 2 進表現で最右ビットの位置)となります。今後はこれをhighestPriorityLane
と呼びます(または、与えられたLanes
数値内で最小のLane
😉)。React の
Lane
は賢く順序付けされています:// 例:任意のLanes数値
// 0b000_1011_0000
// 最優先レーンは0b000_0001_0000したがって、レンダーフェーズアップデートからコンテナを更新する場合、React は最優先レーンを採用します
アップデートが
Transition
内で発生する場合、ここで定義されたTransitionLanes
から選択し、次に使用するトランジションレーンをアップグレードまたはリセットします。root.render()
からトランジションを強制するには、startTransition
でラップします:React.startTransition(() => {
root.render(children);
});グローバルな
currentUpdatePriority
が設定されていてNoLane
(0
)でない場合、それが返されます上記の条件に該当しない場合、React はアップデートが React 外から発生したと判断し、
Host
環境にgetCurrentEventPriority()
を要求します。DOM 環境の場合、window.event
を使用して優先度を推論します
3. サブツリーコンテキストの解決とアタッチ
次に React は、container(FiberRootNode).context
が null の場合、推論してアタッチします。すでに定義されている場合はcontainer.pendingContext
にアタッチします。この context プロパティについては後述します
4. update
オブジェクトの作成
前章で学んだように、FiberNode
にはpending
アップデートを収集するUpdateQueue
があります。ここで最初の本物のUpdate
オブジェクトが作成されます:
// クロージャ変数:
// - element: root.renderに渡されたReactノード(childrenまたはui)
// - callback: updateContainerの最後のパラメータ(root.renderからはnull)
// 簡略化版
const update = {
lane,
tag: UpdateState, // 0
callback, // callbackまたはnull
payload: { element }, // elementはルートの子要素
next: null,
};
5. 作成したアップデートをFiber
にエンキュー
この時点で、updateLane
を認識し、UI をペイロードとして含むFiberRoot
に適用するUpdate
を作成しましたが、すぐには適用されません。React はこのアップデートの処理を適切にスケジュールする必要があります。
そのため、最初のステップとしてこのアップデートをキューに追加します:
const root: FiberRoot | null = enqueueUpdate(current, update, lane);
// current: FiberNode
// update: Update
// lane: number (update lane)
enqueueUpdate
は以下のステップを経由します:
fiber.updateQueue
が null の場合、null
を返します。このパスからのみnull
が返され、このfiber
がアンマウントされたことを意味します開発環境でネストされた
setState
呼び出しについて警告します:同じsetState(prev => next)
内からsetState
を呼び出す場合。この警告はクラスコンポーネントのみに表示され、関数コンポーネントでは最新のsetState
が優先されます。両方のケースを示すCodeSandbox 例があります。setState内でのsetState呼び出し警告
このスニペットは開発環境で警告を表示する方法と、hooksのsetStateからは警告が発生しないことを示しています。// このコードはデモ用です
let instance;
class ClassComponent extends React.Component {
state = { value: "" };
render() {
instance = this;
return this.state.value;
}
}
let setState;
function FunctionComponent() {
let [state, _setState] = React.useState("");
setState = _setState;
return state;
}
function App() {
React.useEffect(() => {
instance.setState(() => {
console.log("クラスコンポーネントのstateを設定中");
// この呼び出しは開発環境で警告を発生させ、毎回setStateと見なします
instance.setState({ value: "Hello !" });
return { value: "この値は無視されます" };
});
setState(() => {
console.log("関数コンポーネントのstateを設定中");
// ここで返されるstateは不安定です。StrictModeが有効かどうかで異なる値が出力されます
// index.jsでunstable_strictMode: falseに変更すると動作が変わります
// ここでも警告が必要です
setState("Another value");
return "World !";
});
}, []);
return (
<div>
<ClassComponent />
<FunctionComponent />
</div>
);
}アップデートがレンダーフェーズのクラスコンポーネントアップデート(関数コンポーネントの
useState
やuseReducer
フックではない)の場合:このアップデートを循環キュー
fiber.updateQueue.shared.pending
に追加します: 既に保留中のアップデートがある場合、新しいアップデートを先頭に配置し、既存の保留中アップデートを参照します。ここで確認できます。 最大 2 要素のpending
キューを処理する際、それらを切り離して2 番目から処理を開始します。ツリーを上方向に走査し、このツリーの
FiberRootNode
を検索します:return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
getRootForUpdatedFiber(fiber)
で最初の走査を行い、fiber.return
がnull
でHostRoot
タグを持つ Fiber を検索します。走査中、React はネストされたアップデートをカウントし、異常を検出するとエラーをスローします。markUpdateLaneFromFiberToRoot(root, null, lane)
による 2 回目の走査では、HostRoot
に到達するまで、遭遇するすべての親にupdateLane
を追加します。OffscreenComponents
の特別なケースについては後述します。
アップデートがクラスコンポーネントのレンダーではない場合(
root.render()
からのデフォルト分岐)、enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane)
を返します:// 定義(簡略化)
export function enqueueConcurrentClassUpdate<State>(): // ... パラメータ
FiberRoot | null {
// {element}をペイロードとするアップデートがここでキューに追加されます
enqueueUpdate(fiber, sharedQueue, update, lane);
// 前出のunsafeレンダーフェーズクラスコンポーネントアップデートで使用した
// getRootForUpdatedFiberが再び使用されます
return getRootForUpdatedFiber(fiber);
}今回の
enqueueUpdate
は以下の処理を実行します(レンダリングに関するコメントは今は無視してください):グローバルな
concurrentQueues
に 4 つの引数をキャプチャ:// 簡略化
concurrentQueues[id++] = fiber;
concurrentQueues[id++] = sharedQueue;
concurrentQueues[id++] = update;
concurrentQueues[id++] = lane;このグローバル変数は React の特定の場所でリセットされます。
updateLane
をグローバルなconcurrentlyUpdatedLanes
に追加:// 簡略化
concurrentlyUpdatedLanes |= lane;レーンを
fiber
(fiberRoot.current
)とそのalternate
にマージ:fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
最終的に、
getRootForUpdatedFiber(fiber)
を通じてHostRoot
が返されます。
いずれの方法でも、FiberRootNode
型のHostRoot
を取得します。enqueueUpdate(currentFiber, update, lane)
の呼び出しはツリーのHostRoot
を返します。
ここで簡単に復習しましょう:
root.render(children)
から始まり、以下の処理を行いました:
fiberRoot.current
(現時点で最初に作成された Fiber)を使用- Transition や root の
mode
など、多くの要因に依存するアップデートレーンを要求 - トップレベルの React
context
オブジェクトを解決 Update
オブジェクトを作成- この
update
をFiber
のupdateQueue
にエンキュー
最後のステップでHostRoot
(FiberNode
プロトタイプ)を取得しました。続きを見ていきましょう。
6. 現在のFiber
アップデートをスケジュール
ここまでで、現在のfiber
はchildren
をペイロードとするupdateQueue
とconcurrentQueues
配列内の変数を持っています。次に、この Fiber のアップデートをスケジュールします:
scheduleUpdateOnFiber(fiberRoot, currentFiber, updateLane);
scheduleUpdateOnFiber
関数は React の多くの場所から呼び出され、レンダーをトリガーする方法です。ステートセッターやフックなどから呼び出されます。
この関数は後ほど何度も登場するため、ここで概要を説明します。現在関係ないコードパスは省略します。
開発環境で
insertion effects
実行中のアップデートスケジュールを警告root
がin progress
かつSuspendedOnData
状態の場合、React が使用するグローバル変数をリセット。root.render(children)
のケースでは関係ないため、後述しますroot
を更新済みとしてマーク:updateLane
をroot
のpendingLanes
プロパティに追加します。これ保留中の作業を表します。このアップデートが Idle でない場合(root.render(children)からのケース) root から 2 つのプロパティ(suspendedLanes と pingedLanes)をリセットします。これは、このアップデートがツリーのブロックを解除したりサスペンド状態を解除したりする可能性があるためです。 これらのプロパティをクリアすることで、ツリーが再レンダーを試行できるようにするためです。
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
root.pendingLanes |= updateLane;
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
}
}アップデートが
render phase update
の場合、他のコンポーネントからの更新を警告通常のアップデートの場合:
ensureRootIsScheduled(root)
を呼び出し:- グローバル変数
firstScheduledRoot
とlastScheduledRoot
に root を登録 - マイクロタスクをスケジュール:
scheduleMicrotask
を使用してprocessRootScheduleInMicrotask
を実行。scheduledRoots
をループ処理します:処理はスケジュールのみされ、実行は後ほど詳細を説明しますscheduleImmediateTask(processRootScheduleInMicrotask);
- グローバル変数
root
がLegacy
の場合、即座にアップデートをフラッシュ
7. Fiber
のトランジションを entangle
このセクションの最後のステップです。ここまで到達したあなたは好奇心と野心の持ち主です。長く複雑なセクションでしたが、これが最後の部分です。
初回レンダーが基本的な場合(例:シンプルなroot.render()
):
createRoot(container).render(children);
この場合、この関数では何も行われません。
しかしstartTransition
でrender
をラップする場合:
function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
const sharedQueue = fiber.updateQueue.shared;
if (isTransition(lane)) {
let queueLanes = sharedQueue.lanes;
queueLanes &= root.pendignLanes;
const newLanes = queueLanes | lane;
sharedQueue.lanes = newQueueLanes;
markRootEntangled(root, newLanes);
}
}
fiber
のshared.lanes
はroot
のpendingLanes
と交差します。これにより両方に存在する共通のレーンのみが残ります次に、このケースで
TransitionLane
を含むupdateLane
とマージされ、fiber.updateQueue.shared.lanes
に割り当てられます最後のステップは
markRootEntangled(root, newQueueLanes)
の呼び出しです。複雑なプロセスのため、段階的に説明します:newQueueLanes
をroot.entangledLanes
に追加ループ前に
root.entanglements
配列を参照lanes
変数として参照し、lanes
が0
でない間ループ:現在の
lanes
インデックスを計算: インデックスは31
から現在のlanes
数値の先頭のゼロの数を引いた値。これは現在のlanes
の 2 進表現で最初の有効ビット(1
)の位置ですlanes
数値が複数のレーンの組み合わせの場合、最上位ビットのlane
は取得したインデックス分左シフトして計算:const lane = 1 << index;
// 例:
// lanes = 21(0b0000_0000_0000_0000_0000_0000_0001_0101)
// clz32(lanes) = 27
// 31 - clz32(lanes) = 4
// 1 << 4 = 0b0000_0000_0000_0000_0000_0000_0001_0000(16)
// これは最優先度が低いレーン(数値が最も高い)を示すレーンが
newQueueLanes
に存在し、newQueueLanes
と推移的に関連している場合、newQueueLanes
をレーンの entanglement(root.entanglements
) に追加:// very simplified
// non-zero will be equal to the lane itself
const laneOrZero = lane & newQueueLanes;
// the existing entanglements at index
const entagledLanesAtIndex = entanglements[index];
// lanes that were entangled intersecting with new queue lanes
// those are lanes that were present already and are coming again
const persistingLanesAtIndex = entagledLanesAtIndex & newQueueLanes;
// this means that either this lane is directly present in the new lanes
// or that it is transitively present from the previous entanglements
if (laneOrZero | persistingLanesAtIndex) {
// add the new lanes to the existing entanglements
entanglements[index] |= newQueueLanes;
}現在の
lane
をlanes
から削除し、lanes
が0
になるまでループ継続
これでupdateContainer(children, root, null, null)
が終了します。
まとめ
root.render(children)
の主な目的は、updateContainer
関数を呼び出し、アップデートオブジェクトを作成してroot._internalRoot.current.shared.pending
にキューイングすることです。この際、children
をelement
としてペイロードに保持します。
この処理中、React はアップデートの発生源について多くのチェックを行います。root.render()
からのケースではほとんどのチェックが該当しませんが、知っておくことが重要です。
現時点で最も重要な部分はscheduleImmediateTask(processRootScheduleInMicrotask)
です。これは後で実行されるコードをスケジュールしますが、まだ詳細は説明していません。これには理由があります:ワークループが開始されるからです。
createRoot(container, options)
とroot.render(children)
の内部動作を見てきましたが、React はまだコンポーネントのレンダーを開始していません。queueMicrotask
を通じて作業をスケジュールしただけです。
これは、React があなたのコンポーネントをindex.js
ファイルのスクリプトが完了した後にレンダーすることを意味します 😉。
root.render(children);
triggerImportantDataFetch();
RegisterServiceWorker();
この機会を利用して、エフェクトなどを経由せずに重要なデータロードを開始し、初期状態でpending
状態に入ったりサスペンドしたりできます。例えば現在のユーザーの解決を開始できます。