説明#
- 本文は v18.1.0 に基づいて分析されています。
- useState のデバッグ中に interleaved という概念が繰り返し現れるので、こちらの リンク を参照してください。
- 記事はおおむね以下のコードに基づいて分析されており、状況に応じて若干の変更があります。
- 2022-10-14 にバッチ処理に関するソースコード分析(タスクの再利用【関連】および 高優先度の割り込み【コード内で軽く触れています】)とスケジューリング段階の簡単なまとめが追加されました。
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1) } }>{ text }</div>
)
}
export default UseState;
TLNR#
- useState の dispatch は非同期操作です。
- useState の実装は useReducer に基づいており、言い換えれば useState は特別な useReducer です。
- useState を呼び出すたびに対応する update インスタンスが生成され、同じ dispatch を複数回呼び出すと、1 つの update の連結リストとして取り付けられます。また、各 update には対応する優先度(lane)が付与されます。
- 生成された update がスケジュール更新が必要と判断されると、microTask の形式でスケジュール更新されます。
- useState の update 段階内では、高優先度の update に迅速に応答するために、update 連結リストは高優先度の update のみを先に処理し、低優先度の update は次回の render 段階で更新されます。
- バッチ処理と(スケジューリング → 調整 → レンダリング)内のスケジューリング段階のまとめは文末に移動してください。
mount シーンでの useState#
useState にブレークポイントを直接設定してデバッグすると、mountState 内に入ります。
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
···
// initialState は useState に渡された初期値です
return mountState(initialState);
···
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// hooks 連結リスト生成手順については[以前の記事](https://github.com/IWSR/react-code-debug/issues/2)を参照してください
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// initialState の型宣言はそれが関数であることも示しています
initialState = initialState();
}
// hook 上の memoizedState、baseState は useState の初期値をキャッシュします
hook.memoizedState = hook.baseState = initialState;
// 更新時に言及される、重要な構造です
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue; // useState に対応する hooks オブジェクトは複数の queue 属性を持ちます
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any)); // 本文の分析の重点、非常に重要な関数です
// const [text, setText] = useState(0); の戻り値に対応します
return [hook.memoizedState, dispatch];
}
このようにして、mount 段階の useState の分析が完了しました。内容は非常に少ないです。
また、basicStateReducer という関数にも注意してください。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
まとめ#
mount 段階の useState は主に初期化の役割を果たし、この関数の呼び出しが終了すると、対応する hooks オブジェクトが得られます。このオブジェクトは以下のようになります。
useState の dispatch をトリガーする#
この時点で、useState が返す dispatch にブレークポイントを設定し、クリックイベントをトリガーします。以下の呼び出しスタックが得られます。
dispatchSetState は重要な関数であることを再度強調します。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/**
* ここでは現在の update の優先度を取得することを理解してください
*/
const lane = requestUpdateLane(fiber);
/**
* setState の update 連結リストに似ています
* ここでの update も連結リスト構造です
*/
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
/**
* レンダリング中の更新かどうかを判断します
*/
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
/**
* React が現在アイドル状態かどうかを判断します
* 例えば、最初の dispatch 呼び出し後、fiber.lanes は NoLanes ではなくなります
* したがって、2 回目の dispatch 呼び出しは下記の eagerState の評価に入ることはありません
*/
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// キューは現在空であるため、次の状態をレンダリングフェーズに入る前に積極的に計算できます。
// 新しい状態が現在の状態と同じであれば、完全にバイアウトできるかもしれません。
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any);
/**
* 期待される状態を計算し、eagerState に保存します
* lastRenderedReducer 内のロジックは以下の通りです
* 現在の action が関数かどうかを判断します
* もちろん setText((preState) => preState + 1) のような呼び出し方も存在します
* もし関数であれば currentState を渡して action を呼び出し、計算された値を得ます
* もしそうでなければ、例えば setText(1) のような呼び出し方であれば、action をそのまま返します
*/
const eagerState = lastRenderedReducer(currentState, action);
// 積極的に計算された状態と、それを計算するために使用されたリデューサーを update オブジェクトに保存します。
// レンダリングフェーズに入るまでにリデューサーが変更されていなければ、eagerState を再度リデューサーを呼び出すことなく使用できます。
update.hasEagerState = true; // コメントに基づいて、リデューサーの再計算を防ぎます
update.eagerState = eagerState;
/**
* 両者の値が等しいかどうかを浅く比較します ===
*/
if (is(eagerState, currentState)) {
// 高速パス。React に再レンダリングをスケジュールする必要がないため、バイアウトできます。
// ただし、異なる理由でコンポーネントが再レンダリングされ、その時点でリデューサーが変更されている場合は、後でこの update を再ベース化する必要があるかもしれません。
// TODO: この場合、トランジションを絡める必要がありますか?
/**
* コメントに基づいて、値が変更されていない場合、React に再レンダリングをスケジュールする必要はありません。
* 下記の関数は単に update 連結リストの構造を処理するだけです。
*/
enqueueConcurrentHookUpdateAndEagerlyBailout(
fiber,
queue,
update,
lane,
);
return; // スケジュールする必要がないため、関数はここで中断します
}
}
}
/**
* ここに到達した update は、ページに再レンダリングされる必要があると見なされます。
* enqueueConcurrentHookUpdate と enqueueConcurrentHookUpdateAndEagerlyBailout は異なり、
* markUpdateLaneFromFiberToRoot を呼び出します。
* この関数は、現在の update に対応する lane を更新が発生した fiber ノードから親 fiber の childLanes に向かって一層一層マークします(fiber.return)。
* ルートまで続きます。
* markUpdateLaneFromFiberToRoot を呼び出す目的は、scheduleUpdateOnFiber のスケジューリングの準備をすることです。
*/
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
/**
* スケジューリングプロセスに入ります(scheduleUpdateOnFiber の分析は最下部に更新されています)
* ただし、v18 では queueMicrotask に登録され、microTask となります。
* v17 では対応する優先度でスケジューラに登録されます。
*/
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
/**
* 不安定な関数で、主に lanes を処理しますが、少し難解で解析しません。
*/
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
useState の dispatch をまとめる#
useState の dispatch に関しては、dispatch を呼び出すたびに新しい update インスタンスが生成され、そのインスタンスは対応する hooks の連結リストに取り付けられますが、dispatch を呼び出した後に生成された新しい値は、元の値(currentState)と浅く比較され、この更新がスケジュール更新される必要があるかどうかを決定します。v18 のバージョンでは、このスケジューリングタスクは microTask 内に存在するため、useState の dispatch は非同期操作であると考えることができます。以下に簡単な例を示して確認します。
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1); console.log(text); } }>{ text }</div>
)
}
export default UseState;
クリック後に得られる結果
その呼び出しスタックでは、microtask が見えます。
update シーンでの useState#
次に、元のデバッグコードに dispatch を追加してみましょう。新しいデバッグコードは次のようになります。
import { useEffect, useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => {
setText(1);
setText(2); // ここにブレークポイントを設定します
} }>{ text }</div>
)
}
export default UseState;
ページ全体の挙動を記録すると、次のようになります。
ちなみに、2 回目の dispatch 時には fiber.lanes === NoLanes という条件を満たさないため、enqueueConcurrentHookUpdate に直接飛び込みます(具体的には dispatchSetState のコードを見てください)。
enqueueConcurrentHookUpdate で update 連結リストが接続された後、次のような環状連結リストの構造が得られます。
本題に戻りますが、useState の update 段階では updateState 内の updateReducer という関数(useReducer に対応する mount バージョン)が呼び出されます。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
/**
* この関数は use(Layout)Effect 内でも登場し、大まかな役割は
* WIP hooks と current hooks のポインタを同時に1つ後ろに移動させることです。
* 詳細な注釈は [React Hooks: 附録](https://github.com/IWSR/react-code-debug/issues/4)を参照してください。
*/
const hook = updateWorkInProgressHook(); // WIP hooks オブジェクト
const queue = hook.queue; // update 連結リスト
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// useState のシーンでは lastRenderedReducer は basicStateReducer です —— mountState で事前に設定されたリデューサーです
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// 最後の再ベース化された update で、ベース状態の一部ではありません。
let baseQueue = current.baseQueue;
// まだ処理されていない最後の保留中の update。
// 保留中の状態の update を取得し、まだ計算されていません。
const pendingQueue = queue.pending;
/**
* update キューを整理し、後続の更新の準備をします。
*/
if (pendingQueue !== null) {
// 新しい更新がまだ処理されていない場合。
// それらをベースキューに追加します。
// pendingQueue と baseQueue を接続します。
if (baseQueue !== null) {
// 保留中のキューとベースキューをマージします。
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
/**
* 処理された baseQueue にはすべての update インスタンスが含まれます。
*/
if (baseQueue !== null) {
// 処理するキューがあります。
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
/**
* renderLanes 内に含まれる update をフィルタリングします。
* このステップはビット操作に関係しています。
* updateLane が renderLanes 内に属する場合、現在の update の優先度が比較的緊急であることを示し、
* 処理する必要があります。逆に、スキップできます。
* 同様のロジックはクラスコンポーネントの update 連結リストにも存在します(ここでは詳細を展開しません)。
* ただし、ここでは ! 操作があるため、すべて反対です。
*/
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 優先度が不十分です。スキップします。このスキップされた最初の update/state が新しいベース
// update/state になります。
/**
* このロジックに入った update は、あまり緊急ではないことを意味し、ゆっくり処理できます。
* しかし、処理しないわけではなく、アイドル状態のときにスキップされた update から再計算を開始します。
* したがって、スキップされた update インスタンスをキャッシュする必要があります。
*/
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
/**
* スキップされた update を newBaseQueue に追加します。
* 次回の render で再計算します。
*/
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 残りのキューの優先度を更新します。
// TODO: これを累積する必要はありません。代わりに、元の lanes から renderLanes を削除できます。
// WIP fiber の lanes を更新し、次回の render でスキップされた update を再実行するためのキーです。
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// スキップされた lanes を workInProgressRootSkippedLanes にマークします。
markSkippedUpdateLanes(updateLane);
} else {
// この update は十分な優先度を持っています。
// 優先度が十分なシーンでは、更新を実行します。
if (newBaseQueueLast !== null) {
/**
* newBaseQueueLast が null でない場合、スキップされた update が存在することを意味します。
* update の状態計算には関連性がある可能性があります。
* したがって、update がスキップされると、スキップされた update を起点として、
* その後のすべての update を優先度に関係なく切り取ります。
* (クラスコンポーネントの処理ロジックに似ています)
*/
const clone: Update<S, A> = {
// この update はコミットされるため、未コミットにすることはありません。NoLane を使用することで、0 はすべてのビットマスクのサブセットであるため、
// これにより、上記のチェックでスキップされることはありません。
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// この update を処理します。
// この update が state 更新(リデューサーではない)であり、積極的に処理された場合、
// 積極的に計算された状態を使用できます。
if (update.hasEagerState) {
// すでに計算された状態にはマークが付けられ、再計算を避けます。
// 状態と action に基づいて新しい状態を計算します。
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 新しい状態が現在の状態と異なる場合、fiber が作業を行ったことをマークします。
// === チェックが通らない場合、現在の WIP fiber に更新をマークし、completeWork 段階で root に収集されます。
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 新しいデータを hook 内に更新します。
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// interleaved updates は別のキューに保存されます。これらはこのレンダリング中に処理されませんが、どの lanes が残っているかを追跡する必要があります。
/**
* interleaved update を処理します。
* ただし、どのような更新が interleaved update と呼ばれるのか全く理解できないため、この部分の分析はスキップします。
* /react-reconciler/src/ReactFiberWorkLoop.old.js 内にこの部分の説明があります。
* レンダリング中のツリーへの更新を受け取りました。これにより、このルートで interleaved update 作業があったことがマークされます。
* ただし、enqueueConcurrentHookUpdate と enqueueConcurrentHookUpdateAndEagerlyBailout の queue.interleaved の設定とは完全に関連していません。
* この分野に関する知識がある方は、ぜひ教えてください。
*/
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` はトランジションを絡めるために使用されます。キューが空になったら、ゼロに戻すことができます。
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
まとめ#
useState の update 段階では、dispatch 関数によって生成された update 連結リストを使用して対応する状態を更新しますが、すべての update が更新されるわけではなく、優先度(lane)が現在の renderLanes 内に属する場合のみ優先的に計算され、スキップされた update はマークされ、次回の render で更新されます。
簡単な例を提供して証明します。
import { useEffect, useState, useRef, useCallback, useTransition } from "react";
function UseState() {
const dom = useRef(null);
const [number, setNumber] = useState(0);
const [, startTransition] = useTransition();
useEffect(() => {
const timeout1 = setTimeout(() => {
startTransition(() => { // 優先度を下げ、timeout2 内の update に割り込まれる
setNumber((preNumber) => preNumber + 1);
});
}, 500 )
const timeout2 = setTimeout(() => {
dom.current.click();
}, 505)
return () => {
clearTimeout(timeout1);
clearTimeout(timeout2);
}
}, []);
const clickHandle = useCallback(() => {
console.log('click');
setNumber(preNumber => preNumber + 2);
}, []);
return (
<div ref={dom} onClick={ clickHandle }>
{
Array.from(new Array(20000)).map((item, index) => <span key={index}>{ number }</span>)
}
</div>
)
}
export default UseState;
実行結果から見ると、timeout1 の update が startTransition によって降格されたため、timeout2 の update が優先的に更新されました。
バッチ処理#
バッチ処理は実際には v17 時代から存在していましたが、v18 ではその制限条件が取り除かれ、自動バッチ処理(厄介)になりました。具体的にはこの 記事 を参照してください。
例えば、次のコードが実行されるとします(もちろん、ここでのデバッグ環境は依然として v18 です)。
import { useEffect, useState } from "react";
function UseState() {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(2);
useEffect(() => {
setNum1(11);
setNum2(22);
}, []);
return (
<>
<div>{ num1 }</div>
<div>{ num2 }</div>
</>
)
}
上記の dispatchSetState の分析に基づくと、ページに更新する必要がある update がある場合、scheduleUpdateOnFiber が呼び出されてレンダリングをトリガーします。したがって、例の中で連続して 2 回 dispatch を呼び出したことになります。つまり、2 回 scheduleUpdateOnFiber がトリガーされました。問題は、ページは何回レンダリングされたのでしょうか?
1 回だけです。
なぜなら、scheduleUpdateOnFiber 内で何が行われているのかを確認する必要があるからです。scheduleUpdateOnFiber は全体の更新の入り口に関わるため、非常に重要な関数です。
この関数は主に以下の処理を行います。
- 無限更新が存在するかどうかをチェックします —— checkForNestedUpdates
- root.pendingLanes に更新が必要な update の lanes をマークします —— markRootUpdated
- ensureRootIsScheduled をトリガーし、タスクスケジューリングのコア関数に入ります。
scheduleUpdateOnFiber#
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ルートが同期的に再レンダリングされる回数をカウントします。
// 完了しない場合、無限更新ループを示します。
checkForNestedUpdates();
// ルートに保留中の更新があることをマークします。
markRootUpdated(root, lane, eventTime);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
/**
* エラーハンドリング、スキップします。
*/
} else {
...
/**
* 重要な関数!タスクスケジューリングのコア
* 私たちが探しているロジックもここにあります。
*/
ensureRootIsScheduled(root, eventTime);
...
// 以下はレガシーモードの互換コードで、見ません。
}
}
ここで、React 内の重要な関数である ensureRootIsScheduled に遭遇します。この関数に入ると、React 内のタスクスケジューリングのコアロジックが見えてきます(Scheduler 内にも一部のコアロジックがあります。私が書いた React Scheduler: Scheduler ソースコード分析 を参照してください)。
ensureRootIsScheduled#
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
/**
* root.callbackNode はスケジューラがスケジューリング時に生成したタスクであり、
* この値は React が scheduleCallback を呼び出すときに返され、root.callbackNode に割り当てられます。
* 既存のコールバックノードの変数名から推測できるように、これはすでにスケジュールされたタスク(旧タスク)を示します。
*/
const existingCallbackNode = root.callbackNode;
// 他の作業によって飢餓状態にある lanes があるかどうかを確認します。もしあれば、それらを期限切れとしてマークします。
markStarvedLanesAsExpired(root, currentTime);
// 次に作業する lanes とその優先度を決定します。
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
/**
* renderLanes が空である場合、現在スケジューリングを開始する必要がないことを意味し、スキップします。
*/
if (nextLanes === NoLanes) {
// 特殊なケース:作業するものがありません。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 最も優先度の高い lane を使用してコールバックの優先度を表します。
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 既存のタスクがあるかどうかを確認します。再利用できるかもしれません。
const existingCallbackPriority = root.callbackPriority;
/**
* もし両方のタスクの優先度が同じであれば、スキップします。すでに対応するタスクが存在するため、再利用できます。
*/
if (
existingCallbackPriority === newCallbackPriority
) {
// 優先度が変わっていないため、既存のタスクを再利用できます。終了します。
return;
}
/**
* 割り込みロジック
* ここに入るロジックはすべて、旧タスクの優先度よりも高いことを意味し、新たにスケジューリングする必要があります。
* したがって、旧タスクは呼び出す必要がなくなり、キャンセルできます(キャンセルロジックはスケジューラ内のロジックを参照してください)。
*/
if (existingCallbackNode != null) {
// 既存のコールバックをキャンセルします。以下で新しいものをスケジューリングします。
cancelCallback(existingCallbackNode);
}
// 新しいタスクをスケジューリングします。
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// 特殊なケース:同期 React コールバックは特別な内部キューにスケジュールされます。
/**
* 同期優先度は、これが期限切れのタスクであるか、現在のモードが非同時モードであることを意味します。
*/
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// マイクロタスク内でキューをフラッシュします。
/**
* ブラウザがマイクロタスクをサポートしている場合、performSyncWorkOnRoot をマイクロタスク内に投げ込みます。
* これにより、より早く実行できます。
* 1 つの eventLoop は、古いマクロタスク → マイクロタスクをクリア → ブラウザがページをレンダリングするという順序で実行されます。
* さもなければ、スケジューラ内に入ると、MessageChannel で処理されたタスクはすべてマクロタスクであり、次の
* eventLoop 内で再レンダリングされます。
*/
scheduleMicrotask(() => {
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
/**
* ここでは、上記でスケジュールされたタスクをキャンセルし、performSyncWorkOnRoot を呼び出します。
*/
flushSyncCallbacks();
}
});
} else {
// 即時タスク内でキューをフラッシュします。
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
// 同時モードの処理ロジック
let schedulerPriorityLevel;
// renderLanes をスケジューリング優先度に変換します。
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;
}
/**
* 対応する優先度でスケジューラに React のタスクをスケジューリングします。
*/
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// root 上のタスクの優先度とタスクを更新し、次回スケジューリングを開始する際に取得できるようにします。
// これは最初に使用した oldTask です。
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
ensureRootIsScheduled を分析したことで、バッチ処理がどのように実現されているのか大まかな理解が得られました。実際には、React 内の同じ優先度のタスクを再利用することによって実現されており、これにより hooks の dispatch を複数回トリガーしても、レンダリングは 1 回だけ行われます。
これで、React Scheduler: Scheduler ソースコード分析 と組み合わせることで、React 内の 3 つの主要なモジュール(スケジューリング → 調整 → レンダリング)のスケジューリング部分をすべて紹介しました。次に、1 つの質問でこの記事全体をまとめましょう。
React は useState の dispatch をトリガーしてからページにレンダリングするまでに何を行ったのか —— スケジューリング編
- 対応する優先度の update インスタンスを生成し、useState に対応する hooks インスタンスの queue 属性に連結リストの形式で取り付けます。
- 現在の update がページにレンダリングされる必要があると判断されると、この update の lane を現在の fiber ノードからその親ノードの childLanes に向かって一層一層マークします(root まで)。
- その後、scheduleUpdateOnFiber を呼び出してスケジューリング段階に入ります。
- まず、現在の update の lanes を root.pendingLanes にマークします。pendingLanes 上のタスクは、すべての待機中のタスクと見なすことができます。
- 次に、pendingLanes 内の最も高い優先度を現在の renderLanes として取得します。
- スケジューラにスケジューリングタスクを登録する前に、すでに登録されているがまだ実行されていないタスク(root.callbackNode)が存在するかどうかを確認します。もし存在する場合、そのタスクの優先度と renderLanes を比較します。
- 優先度が同じであれば、そのタスクを再利用します。
- renderLanes の方が高ければ、旧タスク(root.callbackNode)をキャンセルし、スケジューラ内で新しいタスクを再登録し、返されたタスクを root.callbackNode に割り当てます。