Reactの内部実装を見る(2. mountStateの中身)
これの続き
このmountStateを見ていく
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useState: mountState,
// ...
};
同ファイルに定義元がある
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
引数と返り値の型は見慣れたものなのでok
実際の処理を1行1行見ていくか。まずこれ
const hook = mountStateImpl(initialState);
mountStateImplってなんだ?見ていこう
同ファイルにある。
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialStateInitializer();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
引数はmountStateと同じでinitialState。だが返り値はHook。
Hookの型を見てみるとこれ
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
memoizedStateはuseStateのstateそのものだろう。
baseStateってなんだ?分からんので飛ばす。
baseQueueはUpdateという型を使ってるな。これを見てみよう
baseState、多分こんな感じかも
スナップショットとしての(古い)stateかな
あぁ、あのupdateオブジェクトか
(revertLaneっていうプロパティは初見だ、新しくできた?)
export type Update<S, A> = {
lane: Lane,
revertLane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
};
WIPのFiberツリー(仮想DOM②)への更新内容そのもの。循環リストになってる。
でupdateはFiber.memoizedStateに格納されてる(この記事によると)
んで次、queueというのは何だ?
これか?flushWork関数(タスクを実行する関数)が積まれてるマクロタスク/マイクロタスクのことかも
タスクを取り出して処理するflushWorkという関数を「キュー」に積んで遅延実行されます。ここでいう「キュー」というのは、Reactの独自実装ではありません。ブラウザのJavaScriptが元々持っている「マクロタスク」もしくは「マイクロタスク」という概念です。
最後、nextというプロパティ。Hookが入ってる。
ということはHookも連結リストになってそうだな。
そういえばフックも循環リストだとcallocさんから聞いたことがあるな
mountStateImplの引数と返り値はわかったので処理を見ていこう
まずこれ
const hook = mountWorkInProgressHook();
今度はmountWorkInProgressHookとかいう知らん関数使われてる
これも見ていくか
これ
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
workInProgressHookというのは何だ?WIPのフック、現在作業中のフック...
まぁでも確かにフックを順番に見ていくという記述はこの記事にもあったので、今作業中のフックというのはそのままの意味か。
workInProgressHookがnullだったらこれやってる
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
hookは今作ったばっかの、全部のプロパティの値がnullのフックオブジェクト。
それをworkInProgressHook(現在作業中のフック)に代入。
この時点でworkInProgressHookはnullでなくなる(プロパティの値は全部nullだけど)
で、そのworkInProgressHookをcurrentlyRenderingFiber.memoizedStateに代入。
currentlyRenderingFiberは現在作業中のfiberだろう。そのmemoizedStateプロパティにworkInProgressHookを代入してる
...んんん?てことはFiberという型のmemoizedStateというプロパティは対応するフックを格納しているっぽいな、、!
この記事のこの記述から、Fiber.memoizedStateはupdateオブジェクトを循環リストとして格納してるもんだと勘違いしてた
この循環リストをフックの持ち主であるFiberのmemoizedStateに保存しておきます。
この記述は間違いということか?...とりあえずそれで理解しよう
(循環リストとして保存するのはFiber.memoizedStateではなくFiber.updateQueueっぽいしなぁ)
であればこの記事のこの記述も納得がいくな。なるほど、Fiberとフック(フックス?)は対応してるのか
// Hooks are stored as a linked list on the fiber's memoizedState field.
hooksはfiberにLinkedListとして格納されるようです。実はuseStateには「毎回同じ順番で同じ回数呼び出さないとデータがずれる」という仕様があるのですが、管理が単なるLinkedListだからですね。
フックが単数で入ってるのかフックス(複数)なのか分からんけど。今回のコードだと1つのフックをただ代入してるだけなのでフックか?
と思ったけどこの記事ではHooksとかlistとか言われてるんだよなぁ...
あーでもこのコメント部分を読んだらリストになってるのがmemoizedStateだという意味っぽいぞ
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
じゃあ多分Fiber.memoizedStateはフックスであって対応するフックが1つ以上入るリストだな
とりあえずworkInProgressHook(現在作業中のフック)をそのcurrentlyRenderingFiber.memoizedStateに代入してるということ。FiberとHookを対応させてる。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
このifはファイル内で見つけた最初のuseState、みたいなことか。だからnull
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
}
で、次else
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
どういうことだぁぁ...??
なんかhooksの型やfiber.memoizedStateなどがよくわかんなくて混乱してきた...
GPTと問答したらめっちゃなるほどぉぉになったので一応メモ
Fiber.memoizedStateはフックを1つ格納してる。
だがフック自体は単方向リストで次のフックへの参照をHook.nextで保持している
だからFiber.memoizedStateは実質複数のフックを保持していると言えるよね~みたいな話。
なるほどぉぉぉ
こんなイメージ
function MyComponent() {
const [count, setCount] = useState(0); // フック1
const [name, setName] = useState("React"); // フック2
const [isActive, setIsActive] = useState(false); // フック3
return <div>{count} {name} {isActive ? "Active" : "Inactive"}</div>;
}
Fiber.memoizedState = {
memoizedState: 0, // フック1の現在の状態
baseState: 0,
queue: { ... }, // フック1の更新キュー
next: { // 次のフック(フック2)
memoizedState: "React",
baseState: "React",
queue: { ... },
next: { // 次のフック(フック3)
memoizedState: false,
baseState: false,
queue: { ... },
next: null // 最後のフック
}
}
}
なのでmemoizedStateが直接的に保持しているもの、というか先頭?のフックは一番最初のフックということだな
Fiber.memoizedStateはそんな感じ。つまりただのフックの連結リスト。
(でもそれならmemoizedStateなんて名前じゃなくてhooksListとかにすれば良いのに謎だな)
んでHooks.memoizedStateはただのuseStateのstateそのもの。countの値とかそういうやつ。
これを踏まえて読み直してみよう
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
- workInProgressHookがnullだったら(=はじめてのuseStateフックだったら)
プロパティが全部nullのhookをworkInProgressHookに代入(初期化)する
->workInProgressHookをcurrentlyRenderingFiber.memoizedStateに代入する
= memoizedStateリスト内の最初のフックということになる
// This is the first hook in the list
- workInProgressHookがnullでなかったら(=2回目以降のuseStateフックだったら)
プロパティが全部nullのhookをworkInProgressHook.nextに代入する
(ここまではifと一緒)
->workInProgressHook.nextをworkInProgressHooktに代入する
うーんとこれはどういうことだ?謎すぎる
workInProgressHook.nextに空hookを代入しても、そのあとworkInProgressHooktに空hook代入しちゃうのでnextへの代入が完全に意味無くなるぞ。何の意味があるんだこれ?
つまりこれだと何をしようが最終的にworkInProgressHook.nextはnullになってしまうはずだぞ。
自分の環境でも模擬的に試したらやはりそうだった。nextへの代入は意味無いはず。
うーん、ということで実質これをやってるだけのはずだぞ
workInProgressHook = hook;
つまりすでにcurrentlyRenderingFiber.memoizedStateはあるので、そこへの代入は必要なく、ただこれから作業するという意味でworkInProgressHookにhookを代入(初期化)した感じやな
謎なのでつぶやいといた
んで最後に、ifでもelseでも関係なくworkInProgressHookを返す
return workInProgressHook;
以上でmountWorkInProgressHook関数はok。
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
呼び出し元のmountStateImpl関数に戻る
戻ってきた
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialStateInitializer();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
1行目のこれは、プロパティが全部nullの初期化されたworkInProgressHook。
const hook = mountWorkInProgressHook();
useStateの初期値として受け取ったものが関数であれば、まずその関数をinitialStateInitializerという変数で保存
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
関数ということはこれはuseStateのinitializer function(初期化関数)のことだな
でその実行結果をinitialStateに代入。initailStateが関数から値になったということ。
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
んでコメントがあるが、多分flowの型チェックを無視するのではないだろか
initialStateは最初関数だったのにただの値に変わっちゃうのは確かに型エラーが出そうなのでそれを防ぐために。
Reactのリンタ抑制に似てる。// eslint-ignore-next-line react-hooks/exhaustive-deps
ってやつ
次のスコープ
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialStateInitializer();
} finally {
setIsStrictModeForDevtools(false);
}
}
shouldDoubleInvokeUserFnsInHooksDEVって何だ?
それがtrueだったらsetIsStrictModeForDevtools(true);
という処理をしていることから、strict modeをtrueにすべきかどうかの情報が入っているはず
直訳しても、「フックス開発環境(?)において、ユーザーの関数を二度呼び出すべきかどうか」なのでStrict Mode関連だろうな
じゃあそのsetIsStrictModeForDevtoolsとは?
これ
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
if (consoleManagedByDevToolsDuringStrictMode) {
if (typeof log === 'function') {
// We're in a test because Scheduler.log only exists
// in SchedulerMock. To reduce the noise in strict mode tests,
// suppress warnings and disable scheduler yielding during the double render
unstable_setDisableYieldValue(newIsStrictMode);
setSuppressWarning(newIsStrictMode);
}
if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
try {
injectedHook.setStrictMode(rendererID, newIsStrictMode);
} catch (err) {
if (__DEV__) {
if (!hasLoggedError) {
hasLoggedError = true;
console.error(
'React instrumentation encountered an error: %s',
err,
);
}
}
}
}
} else {
if (newIsStrictMode) {
disableLogs();
} else {
reenableLogs();
}
}
}
1個1個見ていくか
まずconsoleManagedByDevToolsDuringStrictMode
「ストリクトモード時にDevToolsが管理するコンソール」???
定義元はこれ
export const consoleManagedByDevToolsDuringStrictMode = true;
んでReactFiberDevToolsHook.jsファイル内を見るとconsoleManagedByDevToolsDuringStrictModeに何かしらを代入している式は1つも無いので、true確定
本番環境ではfalseを代入するみたいな処理が他であるんじゃないかと予想
とりあえずdevモードならtrueになるみたいな雑なイメージでいく
今見てみたらconsoleManagedByDevToolsDuringStrictMode消されてた。12/14に消されてる
なので今回のif文は通過確定で、その中のスコープはこれ
if (typeof log === 'function') {
// We're in a test because Scheduler.log only exists
// in SchedulerMock. To reduce the noise in strict mode tests,
// suppress warnings and disable scheduler yielding during the double render
unstable_setDisableYieldValue(newIsStrictMode);
setSuppressWarning(newIsStrictMode);
}
logってなんや?
これ
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing
export const log = Scheduler.log;
これは実際にはスケジューラー上には存在しないが、内部テストに必要なscheduler/unstable_mock上には存在する
Schedulerとは何だろう
こうimportされてる
// This module only exists as an ESM wrapper around the external CommonJS
// Scheduler dependency. Notice that we're intentionally not using named imports
// because Rollup would use dynamic dispatch for CommonJS interop named imports.
// When we switch to ESM, we can delete this module.
import * as Scheduler from 'scheduler';
このモジュールは、外部のCommonJS Scheduler依存性のESModulesラッパーとしてのみ存在します。Rollupは、CommonJS相互接続の名前付きインポートに対してダイナミック・ディスパッチを使用するので、意図的に名前付きインポートを使用していないことに注意してください。ESModulesに切り替えたら、このモジュールを削除できます。
関連する前提知識おさらい
なるほど、なんとなく理解。
CommonJSのESMラッパーがSchedulerか
import * as Scheduler from 'scheduler';
んでそのimport先まで更に辿ってみると、ファイルの中身はこれだけ
'use strict';
export * from './src/forks/Scheduler';
さらにたらい回しか。その先のファイルのexportしてる内容を見にいくとこの4か所
export type Callback = boolean => ?Callback;
export opaque type Task = {
id: number,
callback: Callback | null,
priorityLevel: PriorityLevel,
startTime: number,
expirationTime: number,
sortIndex: number,
isQueued?: boolean,
};
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,
NormalPriority as unstable_NormalPriority,
IdlePriority as unstable_IdlePriority,
LowPriority as unstable_LowPriority,
unstable_runWithPriority,
unstable_next,
unstable_scheduleCallback,
unstable_cancelCallback,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield,
requestPaint as unstable_requestPaint,
unstable_continueExecution,
unstable_pauseExecution,
unstable_getFirstCallbackNode,
getCurrentTime as unstable_now,
forceFrameRate as unstable_forceFrameRate,
};
export const unstable_Profiling: {
startLoggingProfilingEvents(): void,
stopLoggingProfilingEvents(): ArrayBuffer | null,
} | null = enableProfiling
? {
startLoggingProfilingEvents,
stopLoggingProfilingEvents,
}
: null;
これらが全部、最終的にはScheduleとしてexportされてるということのはず
あとopaque type
とかいうflow独自の型が使われてるな。定義されたモジュール内部では普通に使えるけど外部には具体的な型の内容は隠蔽できるみたいなやつらしい。
ん?でもそれだとlogというプロパティは見当たらないのでこれがおかしいな...?
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing
export const log = Scheduler.log;
あ、コメントにヒントがあるかも
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing
実際にscheduler上には存在しないが、scheduler/unstable_mockには存在するで、みたいなこと書かれてる
そこ見てみるか
これだけ
'use strict';
export * from './src/forks/SchedulerMock';
そのexport元を見に行くとこれ
あったぞ!おそらくこれだ
function log(value: mixed): void {
// eslint-disable-next-line react-internal/no-production-logging
if (console.log.name === 'disabledLog' || disableYieldValue) {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any values yielding in the second pass.
return;
}
if (yieldedValues === null) {
yieldedValues = [value];
} else {
yieldedValues.push(value);
}
}
export {
...
log
1行1行見ていくか
// eslint-disable-next-line react-internal/no-production-logging
はreactのeslintリンタ抑制みたいなやつだろな
// eslint-ignore-next-line react-hooks/exhaustive-deps
console.log.nameってなんだ?
if (console.log.name === 'disabledLog' || disableYieldValue) {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any values yielding in the second pass.
return;
}
JSでconsole.log.name
と書いてnameの定義元飛んでみたら、こうなってた
interface Function {
/**
* Returns the name of the function. Function names are read-only and can not be changed.
*/
readonly name: string;
}
なるほど、関数名のことっぽい
例えば以下のようになる
console.log(console.log.name); // 出力: log
function fire() {
console.log('aiueo');
}
console.log(fire.name); // 出力: fire
console.logのlogというのは関数であって当然その関数名もlogであるということやな
ただそれで言うと、console.log.name === 'disabledLog'
というのはなんだ?絶対logになるのではないのか?
if (console.log.name === 'disabledLog' || disableYieldValue) {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any values yielding in the second pass.
return;
}
うーん、どこかで'disableLog'に変えられる処理があるのか?
...いや、無かった
じゃあもっと低レイヤーで自動的に切り替えられるみたいな話なのかもしれない。
とにかく、console.log.name === 'disabledLog'
になっていたらreturnで強制終了している
それか、disableYieldValueがtruthyであってもreturnで強制終了
disableYieldValueというのは何だ?
「valueを生めない(収穫できない)」かぁ...わからん
同ファイルに定義があるが、falseが代入されてるだけ
var disableYieldValue = false;
更新用関数もあるな
function setDisableYieldValue(newValue: boolean) {
disableYieldValue = newValue;
}
直接disableYieldValue =
という感じで代入されてるとこは無さそうなので、おそらくこの更新用関数で更新されてるはず
んでこの更新用関数は違う名前でexportされてる
export {
...
setDisableYieldValue as unstable_setDisableYieldValue,
なのでunstable_setDisableYieldValueが使われてる場所を探してみよう
あった。これだ
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
if (consoleManagedByDevToolsDuringStrictMode) {
if (typeof log === 'function') {
// We're in a test because Scheduler.log only exists
// in SchedulerMock. To reduce the noise in strict mode tests,
// suppress warnings and disable scheduler yielding during the double render
unstable_setDisableYieldValue(newIsStrictMode);
setSuppressWarning(newIsStrictMode);
}
ん、まさかの偶然setIsStrictModeForDevtools関数に戻ってきた
ぐるぐる回った結果同じとこに戻ってきたのおもろい
logを発見
↓
調べた結果logを探し当てる
↓
logの中身を見てるとdisableYieldValueを発見
↓
その中身を探してたらlogを発見した場所に戻ってきた
つまり相互作用しあってる感じなんだろうな
まぁとにかくnewIsStrictModeが実質的な値であることがわかる。
newIsStrictModeはsetIsStrictModeForDevtoolsの引数なので、setIsStrictModeForDevtoolsがどのように使われてるのか探せば良い
結構使われてるけど、まぁ予想できるように、開発中はtrueにすることで2回レンダーするよ、みたいなことをする必要があるが、そのtrueの部分
if (shouldDoubleRenderDEV) {
// In development, components are invoked twice to help detect side effects.
setIsStrictModeForDevtools(true);
なのでdisableYieldValueは実質、「strictModeかどうか≒レンダー2回すべきかどうか」みたいに雑に捕えよう
(でも変数名からはまったく想像できないものになってしまうのでちょっと謎だが...。まぁいい)
理解したので戻るか
log関数の続きから
function log(value: mixed): void {
// eslint-disable-next-line react-internal/no-production-logging
if (console.log.name === 'disabledLog' || disableYieldValue) {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any values yielding in the second pass.
return;
}
if (yieldedValues === null) {
yieldedValues = [value];
} else {
yieldedValues.push(value);
}
}
console.log.nameがdisableLogに変えられていたら、もしくはdisableYieldValueがtrueつまりstrictModeだったら(?)、returnで強制終了
んでコメントにこう書かれてる
console.logにパッチが適用されている場合は、レンダリング再生中であるとみなし、2回目のパスで得られた値はすべて無視する
ほー、strict modeによる2回目のrenderだったらconsole.log.nameがdisableLogにパッチされているということかも。その場合は「レンダリング再生中」とみなして何もしないという感じか?
んで最後ここ
if (yieldedValues === null) {
yieldedValues = [value];
} else {
yieldedValues.push(value);
}
yieldedValuesは同ファイルでこう定義されてる。まぁyieldValueが入った配列だろうな
let yieldedValues: Array<mixed> | null = null;
つまり引数であるvalueを配列内に入れてる(何のためかは知らん)
log関数終了。setIsStrictModeForDevtoolsに戻る
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
if (consoleManagedByDevToolsDuringStrictMode) {
if (typeof log === 'function') {
// We're in a test because Scheduler.log only exists
// in SchedulerMock. To reduce the noise in strict mode tests,
// suppress warnings and disable scheduler yielding during the double render
unstable_setDisableYieldValue(newIsStrictMode);
setSuppressWarning(newIsStrictMode);
}
if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
try {
injectedHook.setStrictMode(rendererID, newIsStrictMode);
} catch (err) {
if (__DEV__) {
if (!hasLoggedError) {
hasLoggedError = true;
console.error(
'React instrumentation encountered an error: %s',
err,
);
}
}
}
}
} else {
if (newIsStrictMode) {
disableLogs();
} else {
reenableLogs();
}
}
}
logの型がfunctionだったら通過
コメント直訳するとこれ
Scheduler.logはSchedulerMockの中にしか存在しないので、テストの中にいる。ストリクト・モードのテストでノイズを減らすには、警告を抑制し、ダブル・レンダリング中のスケジューラの降伏を無効にする。
mockはテストだと考えて良さそう。それ以外は意味わからん。
んでこれ
unstable_setDisableYieldValue(newIsStrictMode);
さっき見た通りこれは実質setDisableYieldValueで、中身はこれ
function setDisableYieldValue(newValue: boolean) {
disableYieldValue = newValue;
}
つまり引数をdisableYieldValueというstrict modeかどうかみたいな謎の変数に代入するだけ
んで次これ。ここからがまだ見てない。
setSuppressWarning(newIsStrictMode);
定義元はこれ
// We expect that our Rollup, Jest, and Flow configurations
// always shim this module with the corresponding environment
// (either rn or www).
//
// We should never resolve to this file, but it exists to make
// sure that if we *do* accidentally break the configuration,
// the failure isn't silent.
export function setSuppressWarning() {
// TODO: Delete this and error when even importing this module.
}
意味無さそうやな。とばす
setIsStrictModeForDevtools関数内の残りは見る意味薄そうだし飛ばすか。
まぁ要は、実質strictmodeにする(ためにいろいろやる)関数って感じかな(知らん)
mountStateImplに戻る
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialStateInitializer();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
mountWorkInProgressHookって何だったか忘れたので見直した
まぁ要はfiberが持ってるhookの連結リストを初期化する、もしくは追加していく、てことやね
とはいえそのhookは全プロパティがnullの初期化されたものなのは同じ。
んで最後にその初期状態のhookをreturnして終わり。
(何なら循環リストか?)
なのでその初期状態hookをhookとして取得。
次のスコープ
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialStateInitializer();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
更新用関数を実行するとこまではもうやったので割愛。
んでstrict modeのような2度実行が必要だったらstrict modeをtrueにする
んで2度目の更新用関数実行
最後にstrict mode(による2度実行の情報?)をオフ
最後のスコープ
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
更新用関数の実行結果としてのinitialStateもしくはstateそのものとしてのinitialStateをhook.baseStateとhook.memoizedStateに代入
てかHook.baseStateってなんだ?
GPTに聞いたらなるほどぉになったのでメモ
まぁbaseStateがスナップショットつまり過去の値で、差分比較のために使われるbaseとしてのstateみたいな感じっぽいな
んでmemoizedStateは常に更新される最新のstate
次の行、UpdateQueueってなんだ?
同ファイルに定義ある
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
Fiber.updateQueueと同じだと思うんだよなぁ、これはただのmixed型だったけど、どっかで繋がってるんじゃないかなぁという妄想
とりあえずUpdateオブジェクトをpendingプロパティとして持っているな
Updateは同ファイルにある、おなじみのこれ
export type Update<S, A> = {
lane: Lane,
revertLane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
};
S, AはState, Actionだろうな
まぁ要はupdateQueueはupdateオブジェクトやその他もろもろを格納してる
んでupdate自体は連結リストになってる
復習。BasicStateActionは懐かしのこれ
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
つまりsetStateの引数
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
setStateそのものがDispatch<BasicStateAction<S>>
ということで戻ると、
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
UpdateQueue型のqueueという変数を作ってる
だがpendingもdispatchもnullなので初期状態に近い?
とはいえlastRenderedState: (initialState: any)
でinitialStateは入れてあげてるので完全初期状態ではないって感じ
んで.memoizedState以外はnullのままの半分初期状態みたいな感じのhookのqueueプロパティにさっきのinitialStateを反映したqueueを代入してあげる
でそのhookをreturnして終了
なのでmountStateImplをまとめると...
- mountWorkInProgressHookにより、fiberが持ってるhookの連結リストを初期化/追加
- 更新用関数だったら2度実行、その後strictmode(の情報?)をオフに
- 最終的なinitialStateをhookに反映させる
- 新しいinitialStateを反映したqueueもhook.queueに代入
- 最後に結果としてできあがったhookをreturn
まぁ要はinitialStateをhookに反映させて、そのhookを返却するよ~ていう関数やね
よっしゃ、mountStateImplが終わったのでやっとmountStateに戻れる、戻ろう
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
ふむふむ
-
const hook = mountStateImpl(initialState);
でinitialStateを反映したhookを取得 -
そのhookの.queueをqueueという変数名で取得
-
dispatchという関数を作る
- 型が
Dispatch<BasicStateAction<S>>
であることからこのdispatchというのはsetState関数そのものであることがわかる
- 型が
だが代入の内容が複雑だな、言語化したい
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
あーbindか、前見たなぁ
ん、これか
じゃあdispatchSetStateというのが何なのかさえわかれば良さそうだな
同ファイルにあった。これ
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
if (__DEV__) {
if (typeof arguments[3] === 'function') {
console.error(
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',
);
}
}
const lane = requestUpdateLane(fiber);
const didScheduleUpdate = dispatchSetStateInternal(
fiber,
queue,
action,
lane,
);
if (didScheduleUpdate) {
startUpdateTimerByLane(lane);
}
markUpdateInDevTools(fiber, lane, action);
}
arguments使ってるな
なるほど、bindしてるから呼び出されるときは引数省略されるとはいえ、dispatchSetStateとしてはすでに引数3つまで読んでることになってて、プラス1個つまりarguments[3]があった場合は、それはsetStateの引数に第二引数が呼ばれたということで、それはおかしいのでエラーを出すという感じだろうな
次、const lane = requestUpdateLane(fiber);
のrequestUpdateLaneとはなんだ
定義元これ
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
if (!disableLegacyMode && (mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
const transition = requestCurrentTransition();
if (transition !== null) {
if (__DEV__) {
if (!transition._updatedFibers) {
transition._updatedFibers = new Set();
}
transition._updatedFibers.add(fiber);
}
const actionScopeLane = peekEntangledActionLane();
return actionScopeLane !== NoLane
? // We're inside an async action scope. Reuse the same lane.
actionScopeLane
: // We may or may not be inside an async action scope. If we are, this
// is the first update in that scope. Either way, we need to get a
// fresh transition lane.
requestTransitionLane(transition);
}
return eventPriorityToLane(resolveUpdatePriority());
}
fiberを受け取ってレーン(優先度)を返す
すごい長いコメントがある。直訳するとこうなる
これはレンダーフェーズのアップデートです。これらは公式にはサポートされていません。古い動作は、現在レンダリングしているものと同じ「スレッド」(レーン)を与えることです。そのため、同じレンダリングの後半で発生するコンポーネントで
setState
を呼び出すと、フラッシュされます。理想的には、特殊なケースを取り除き、インターリーブされたイベントから来たかのように扱いたい。いずれにせよ、このパターンは公式にはサポートされていません。この動作はフォールバックに過ぎない。既存のコードが誤って現在の動作に依存してしまう可能性があるため、setState警告をロールアウトできるようになるまで、このフラグは存在するだけです。
全然意味わからん。ちなみにインターリーブは「割り込む or 構成要素を入れ替える/交互に配置する」みたいな意味っぽい
うーん細かくは見ないことにするがとりあえず、引数で受け取ったfiberに適切なLaneを計算して返す、みたいな関数だろうな
前見たように、Fiberにも.lanesというプロパティがあるので
dispatchSetStateに戻る
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
if (__DEV__) {
if (typeof arguments[3] === 'function') {
console.error(
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',
);
}
}
const lane = requestUpdateLane(fiber);
const didScheduleUpdate = dispatchSetStateInternal(
fiber,
queue,
action,
lane,
);
if (didScheduleUpdate) {
startUpdateTimerByLane(lane);
}
markUpdateInDevTools(fiber, lane, action);
}
次、dispatchSetStateInternalとは何だ?
すぐ下に定義があった
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
if (__DEV__) {
prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactSharedInternals.H = prevDispatcher;
}
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}
fiber, queue, action. laneを受け取ってbooleanだけを返すのか
とりあえず最初のisRenderPhaseUpdateとenqueueRenderPhaseUpdateを見てみる
同ファイルに定義がある
function isRenderPhaseUpdate(fiber: Fiber): boolean {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
): void {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
isRenderPhaseUpdateはfiberを受け取って、そのfiberがrender中のものかどうかをbooleanで返す
enqueueRenderPhaseUpdateはqueue, updateを受け取って、updateオブジェクトの循環リストを作っていく感じ
GPT
たしかにrender中にsetStateが呼ばれたみたいなときにここを通過するのかも。
だから特別な処理が行われるのも納得できる
つまりこの部分は、「render中だったらそのupdateを循環リストに突っ込んどく」みたいなことをしてる感じか
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}
じゃあレンダー中じゃなかったらどうするの?すぐ更新を反映するみたいなこと?
ということでelse下を見ていく
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
if (__DEV__) {
prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactSharedInternals.H = prevDispatcher;
}
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}
というか更新はレンダー中ではないのが普通よな。
ボタンを連続でクリックしまくるみたいな時にのみrender中になってしまう可能性がある程度だと思うので。そんなの特殊ケースな気がする
const alternate = fiber.alternate;
Fiber.alternateは「差分検出用の古いfiberへのリンク」と把握している
fiberもしくはalternateのlanesがNoLanesだった時のみ通過する ←??
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
)
ちなみにNoLaneはレーンの中で一番優先度が高いデフォルト値
特殊なレーンとして「NoLane」という全てのビットが0がレーンがあります。
これはJavaScriptでいう所のundefinedのような、レーンを何も設定していない際のデフォルト値であり、最も高い優先度です。(もっとも、「NoLane」を優先度として使うことは稀です)
んー、てことはfiberができたてホヤホヤだからレーンもデフォルト値のままで、その場合にのみ処理をしていきたい、みたいなことなのだろうか
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
コメントを直訳するとこれ
キューは現在空なので、レンダーフェーズに入る前に次の状態を熱心に計算することができる。新しい状態が現在の状態と同じなら、完全にドタキャンできるかもしれない。
bail outは救済という意味だが、どうやらドタキャンという意味もあるらしく、それはつまりrenderをスキップするという意味のドタキャンでは無いだろうか。「新しい状態が現在の状態と同じなら」って言ってるし
それを頭の隅に入れた状態で読み進めていく
const lastRenderedReducer = queue.lastRenderedReducer;
queueはUpdateQueue型なので復習すると、こんな型
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
なのでlastRenderedReducerはstateとactionを受け取ってstateを返す(もしくはnull)ことがわかる
あーこれはつまりuseReducerのreducerそのものだな。useStateは関係無さそうだし飛ばすか
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
おそらく、スナップショットとしてのreducer、つまり直近で使われてた古いreducer的な意味合いか?
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
if (__DEV__) {
prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
-
lastRenderedReducerがnullじゃなかったらif文通過。
-
prevDispatcherをnullで初期化。
-
__DEV__
つまり開発環境だったら更にif文通過。 -
prevDispatcherに懐かしのReactSharedInternals.Hを代入。ReactSharedInternals.Hはフックがたくさん入ってるDispatcher型のやつ↓
export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
...
const ReactSharedInternals: SharedStateClient = ({
H: null,
...
export default ReactSharedInternals;
- ReactSharedInternals.HにInvalidNestedHooksDispatcherOnUpdateInDEVを代入。
InvalidNestedHooksDispatcherOnUpdateInDEVってやつもDispatcherか。
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
InvalidNestedHooksDispatcherOnUpdateInDEVの代入を見てみたい
これ
InvalidNestedHooksDispatcherOnUpdateInDEV = {
...
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactSharedInternals.H = prevDispatcher;
}
},
...
warnInvalidHookAccess()はこれ
const warnInvalidHookAccess = () => {
console.error(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
'You can only call Hooks at the top level of your React function. ' +
'For more information, see ' +
'https://react.dev/link/rules-of-hooks',
);
};
なのでフックが無効な使い方されたときの警告をだすためだけのフックだな
たしかにInvalidNestedHooksDispatcherOnUpdateInDEVという名前の通り。
うーんなんでlastRenderedReducerが存在するかつ__DEV__
だったら無効なフックになるのかわからんが、次にいくか
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
queue.lastRenderedStateというのはさっきの「直近のreducer」のstate版かな
つまり「直近のstate」。差分で使う感じやろな
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
ん、でもそれってHook.baseStateと同じじゃね...?使い分けが気になるが一旦無視
とりあえずcurrentStateには直近の古いstateを代入してるということが言える
const currentState: S = (queue.lastRenderedState: any);
次の行
const eagerState = lastRenderedReducer(currentState, action);
このlastRenderedReducerという関数はさっき代入したやつ
const lastRenderedReducer = queue.lastRenderedReducer;
つまり直近の古いreducer関数へのリンク
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
なのでその古いreducer関数を実行してることになる
const eagerState = lastRenderedReducer(currentState, action);
引数はcurrentState(古い直近のstate)とaction。その返り値がeagerStateとして代入される。
なるほど、たしかにreducerは更新処理を実行し、結果としてのstateを返すのでそれをeagerStateとして取得する訳か
次
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
コメントを直訳
熱心に計算されたstateと、それを計算するために使用されたreducerをupdateオブジェクトに格納します。レンダリングフェーズに入るまでにリデューサーが変更されていなければ、リデューサーを再度呼び出すことなく、eagerされたstateを使用することができます。
あーstashって「へそくり」とか出てくるから意味をよくわかっていなかったんだけど、「格納」って意味なんだ
てか相変わらずeagerの意味がよくわからん。熱心てなんやねん。
あ、なるほどLazyの対極か。理解
Eager Loading ↔ Lazy Loading
そんなことしてなくない?stateを入れてるだけでreducerは入れてないように見えるが...一旦飛ばす
熱心に計算されたstateと、それを計算するために使用されたreducerをupdateオブジェクトに格納します
次
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
is()とは?Object.isじゃないのか?
定義元ファイル、これ
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
// $FlowFixMe[method-unbinding]
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
お、激アツだ!「Object.isって差分検出で実際に使われてんのか?」てずっと気になってたので見れて良かった
state の同一性の比較は、Object.is によって行われます
Object.isだけじゃなくて、うまく拡張してる感じか
今回、isという名前でimportしてるが、
import is from 'shared/objectIs';
実態としてはobjectIs関数なので、「isという名前でObject.isを使う慣習がある」と把握しておいても良さそうやな
export default objectIs;
コメント直訳
Object.isポリフィルをインライン化し、コンシューマが独自に出荷する必要がないようにした。
ポリフィルとは互換性保つためのコード的な感じか
Object.isの型が関数だったら普通にObject.is使うけど、もし関数じゃなかったらisという独自関数を使ってObject.isの代わりにするみたいな感じなのかな。
関数じゃない場合と言うのは、そうなってしまうReactユーザの環境?が多分あるんだろうな。
とりあえず、is関数というポリフィル(互換関数)も用意してあるよ~という感じ。
GPT
なるほど、Object.isはES6からなのか
Object.is は ES6(ECMAScript 2015)で導入されたため、古いブラウザや環境では利用できない場合があります。
- 環境が Object.is をサポートしている場合にのみ、そのまま利用します。
- もしサポートされていない場合は、is 関数というポリフィル(互換関数)を代わりに使用します。
なるほど
例えば、ES5以前の環境では Object.is が存在しません。
ということで戻る
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
is()の引数の2つはこんな感じなので、
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
以下2つを比較すると言える。
- currentState = 直近の古いstate
- eagerState = 直近の古いreducerを使っているがその引数のactionは新しいやつで、その実行結果としてのstate
- actionはbindされた結果としてのdispatch関数(実質setState関数)の第一引数に渡されるものなので、新しく渡されたものと考えて良いはず(mountState関数から辿ればそう判断できる)
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
コメントを直訳
高速パス。Reactの再レンダリングをスケジューリングすることなくベイルアウト(renderをドタキャン)できる。
コンポーネントが別の理由で再レンダリングし、その時までにreducerが変更されていた場合、この更新を後でリベースする必要がある可能性はまだある。
TODO: この場合でもまだtransitionsを絡める必要があるか?
- renderをドタキャンできるケースのことをFast path/高速パスと呼ぶっぽい
- 「でもまだリベースすべきかもしれんから油断すんな」 ← ???
- TODOで問いかけてんのは何?誰に対しての問いかけ?そしてどういう意味?
GPT
そりゃそうじゃね?としか思わないというか、よくわからない...
なるほど、確かにソースコードのコメントなんだからReact開発者へのメッセージか。
このスキップする処理がtransitionに関係するなら、考えないといけないことがあるかもよ、みたいなことか。transition自体よくわからんのでイメージできないが、とりあえずok
てことはこのenqueueConcurrentHookUpdateAndEagerlyBailout関数はrenderをスキップする処理なんじゃなかろうか。見てみる
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
これ
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
): void {
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
// Usually we can rely on the upcoming render phase to process the concurrent
// queue. However, since this is a bail out, we're not scheduling any work
// here. So the update we just queued will leak until something else happens
// to schedule work (if ever).
//
// Check if we're currently in the middle of rendering a tree, and if not,
// process the queue immediately to prevent a leak.
const isConcurrentlyRendering = getWorkInProgressRoot() !== null;
if (!isConcurrentlyRendering) {
finishQueueingConcurrentUpdates();
}
}
引数の型で使われてるHookQueueとHookUpdateってなんや?て思ったけどなぜかas
で名前変えてimportしてるだけで、実態はおなじみUpdateQueueとUpdateのこと
import type {
UpdateQueue as HookQueue,
Update as HookUpdate,
} from './ReactFiberHooks';
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
この関数は、再レンダーを必要としない更新をキューに入れるために使用される。
へぇ、再レンダー不要のupdateもキューに突っ込むんだ。
...なんで?保持しておく理由なんかあるのか?変更が起こらないupdateなんてもはやupdate(更新)ではないから捨てちゃえば良いのに。使うのか?
キューに入れる唯一の理由は、その後に優先順位の高い更新があり、その更新がリベースされる場合です。
うーん、よく意味がわからない。rebase(状態の再適用)することになったらそのupdateを元にrebaseしたいからupdateは必要だ、みたいな感じ?わからん。飛ばす
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
laneをNoLaneで初期化。
ConcurrentQueueとConcurrentUpdateとは?さっきと同じでUpdateQueueとUpdateのことか?
→違った。独自に作られたやつか。UpdateQueueとUpdateを簡易化したというか、不要なプロパティをなくした版みたいな感じか
export type ConcurrentUpdate = {
next: ConcurrentUpdate,
lane: Lane,
};
type ConcurrentQueue = {
pending: ConcurrentUpdate | null,
};
UpdateQueueとUpdateはこれ。再掲
export type Update<S, A> = {
lane: Lane,
revertLane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
};
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
でもそれだと代入できないはずでは?
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
代入されてるqueueとupdateは引数で渡ってきたものであって、それは実質UpdateQueueとUpdateだからなぁ。ConcurrentQueueとConcurrentUpdateには適合できないはず
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
): void {
うーん、flowでは一致したプロパティがあるなら強制的に型変換することが可能なのか?
そう理解するしかないな
ということで次
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
enqueueUpdateはこれ
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
// Don't update the `childLanes` on the return path yet. If we already in
// the middle of rendering, wait until after it has completed.
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
// The fiber's `lane` field is used in some places to check if any work is
// scheduled, to perform an eager bailout, so we need to update it immediately.
// TODO: We should probably move this to the "shared" queue instead.
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
concurrentQueuesとconcurrentQueuesIndexはこれ
// If a render is in progress, and we receive an update from a concurrent event,
// we wait until the current render is over (either finished or interrupted)
// before adding it to the fiber/hook queue. Push to this array so we can
// access the queue, fiber, update, et al later.
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
訳
レンダーが進行中であり、並行イベントからupdateを受け取った場合、現在のレンダリングが完了するか中断されるまで、updateをfiber/hookキューに追加するのを待ちます。
この配列に追加しておき、後でキューや fiber、updateなどにアクセスできるようにします。
ふむふむ、updateを受け取ったとしてもそれがrender中だったら、そのupdateをfiber/hookのqueueに入れたくないので、このconcurrentQueuesに入れておくよ、みたいな感じか
確かに前見た通りFiber.updateQueue
もHook.baseQueue
もupdateの循環リストなので、そこに直接入れないよ、てことね
へぇ、てことは直接fiber/hookのキューに入れるupdateはrender中では無いやつのみなんだ。
とはいえrenderが終わったらrender中のupdate関連であるfiber, update, キューとかにアクセスできるようにするために、concurrentQueuesにそれらの情報をpushしておくのね。
へぇぇ、なんかいろいろ考えられててすごいなぁ
concurrentQueuesなので、レンダー中のupdateを入れるというより、たとえレンダー中でなくても並行レンダーで発生したupdateがあればそれを入れるみたいな感じかもしれない
...ただ、そうなると矛盾が生じないか?
今見てるコードの大元はこのdispatchSetStateInternalな訳だが、
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
if (__DEV__) {
prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactSharedInternals.H = prevDispatcher;
}
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}
今はこのisRenderPhaseUpdate(fiber)
の結果がfalseとしてelse内に入っているので、render中の更新ではないことが確定しているはず。
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
render中ではないスコープなのに、そこでrender中のupdateのためのenqueue処理をしているということよね。これっておかしくないか???
メモ:render中に更新するというのはコンポーネントのトップレベルでsetState実行するのと同じはずなので、それによる無限ループというあの謎を解明することに繋がるかもしれない...
(あれ、0を0に変更するのでも無限ループするのが謎なんだよな...)
function MyComponent() {
const [count, setCount] = useState(0);
// render中に状態更新を行う
setCount(count + 1); // NG: render中に状態更新を発生させている
return <div>{count}</div>;
}
ふーむ、GPTに聞いたらこんな感じだった
-
isRenderPhaseUpdate(fiber)
がfalseであることは、確かに「現在のfiberがrenderフェーズの更新対象でない」ことを意味する - だが、Reactアプリ全体では「並行的なレンダリング」が進行している可能性があり、その場合レンダリング中のfiber以外から並行して更新が発生し得る
- concurrentQueuesはその並行的に発生した更新を一時的に溜めておく役割(後続で安全に処理する)
- つまりrender中でなくても並行して別のレンダリングやイベント処理が発生する可能性があり、そういったものをカバーするのがconcurrentQueuesである
疑問
- 並行レンダリングとは?まさかレンダーって並行処理されてんの?
- レンダーなのかレンダリングなのか
- concurrentQueueとconcurrentQueuesの違いは?
- render中でなくても並行レンダーによるupdateをconcurrentQueuesが受け取るのはわかったが、render中のupdateも受け取るという認識で良い?コードのコメントを見る限りそうだと思うんだが
疑問1:並行レンダリングとは?
別スクラップに分離して見ていくか