Open195

Reactの内部実装を見る(2. mountStateの中身)

Yug (やぐ)Yug (やぐ)

このmountStateを見ていく

packages\react-reconciler\src\ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  // ...
  useState: mountState,
  // ...
};
Yug (やぐ)Yug (やぐ)

同ファイルに定義元がある

ReactFiberHooks.js
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行見ていくか。まずこれ

ReactFiberHooks.js
const hook = mountStateImpl(initialState);

mountStateImplってなんだ?見ていこう

Yug (やぐ)Yug (やぐ)

同ファイルにある。

ReactFiberHooks.js
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。

Yug (やぐ)Yug (やぐ)

Hookの型を見てみるとこれ

ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

memoizedStateはuseStateのstateそのものだろう。

baseStateってなんだ?分からんので飛ばす。

baseQueueはUpdateという型を使ってるな。これを見てみよう

Yug (やぐ)Yug (やぐ)

あぁ、あのupdateオブジェクトか
(revertLaneっていうプロパティは初見だ、新しくできた?)

ReactFiberHooks.js
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に格納されてる(この記事によると)
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#1.-フックから循環リストを作成

Yug (やぐ)Yug (やぐ)

んで次、queueというのは何だ?

これか?flushWork関数(タスクを実行する関数)が積まれてるマクロタスク/マイクロタスクのことかも

タスクを取り出して処理するflushWorkという関数を「キュー」に積んで遅延実行されます。ここでいう「キュー」というのは、Reactの独自実装ではありません。ブラウザのJavaScriptが元々持っている「マクロタスク」もしくは「マイクロタスク」という概念です。

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#2.タスクを処理する関数をマクロタスクとして追加


最後、nextというプロパティ。Hookが入ってる。

ということはHookも連結リストになってそうだな。
そういえばフックも循環リストだとcallocさんから聞いたことがあるな
https://x.com/calloc134/status/1861254365543309792

Yug (やぐ)Yug (やぐ)

mountStateImplの引数と返り値はわかったので処理を見ていこう

まずこれ

ReactFiberHooks.js
const hook = mountWorkInProgressHook();

今度はmountWorkInProgressHookとかいう知らん関数使われてる

これも見ていくか

Yug (やぐ)Yug (やぐ)

これ

ReactFiberHooks.js
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のフック、現在作業中のフック...

まぁでも確かにフックを順番に見ていくという記述はこの記事にもあったので、今作業中のフックというのはそのままの意味か。
https://gist.github.com/mizchi/fa00890df2c8d1f27b9ca94b5cb8dd1d

workInProgressHookがnullだったらこれやってる

ReactFiberHooks.js
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

hookは今作ったばっかの、全部のプロパティの値がnullのフックオブジェクト。

それをworkInProgressHook(現在作業中のフック)に代入。
この時点でworkInProgressHookはnullでなくなる(プロパティの値は全部nullだけど)

で、そのworkInProgressHookをcurrentlyRenderingFiber.memoizedStateに代入。

currentlyRenderingFiberは現在作業中のfiberだろう。そのmemoizedStateプロパティにworkInProgressHookを代入してる

Yug (やぐ)Yug (やぐ)

...んんん?てことはFiberという型のmemoizedStateというプロパティは対応するフックを格納しているっぽいな、、!

この記事のこの記述から、Fiber.memoizedStateはupdateオブジェクトを循環リストとして格納してるもんだと勘違いしてた

この循環リストをフックの持ち主であるFiberのmemoizedStateに保存しておきます。

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#1.-フックから循環リストを作成

この記述は間違いということか?...とりあえずそれで理解しよう
(循環リストとして保存するのはFiber.memoizedStateではなくFiber.updateQueueっぽいしなぁ)

であればこの記事のこの記述も納得がいくな。なるほど、Fiberとフック(フックス?)は対応してるのか

// Hooks are stored as a linked list on the fiber's memoizedState field.
hooksはfiberにLinkedListとして格納されるようです。実はuseStateには「毎回同じ順番で同じ回数呼び出さないとデータがずれる」という仕様があるのですが、管理が単なるLinkedListだからですね。

https://sbfl.net/blog/2019/02/09/react-hooks-usestate/

フックが単数で入ってるのかフックス(複数)なのか分からんけど。今回のコードだと1つのフックをただ代入してるだけなのでフックか?

と思ったけどこの記事ではHooksとかlistとか言われてるんだよなぁ...

あーでもこのコメント部分を読んだらリストになってるのがmemoizedStateだという意味っぽいぞ

ReactFiberHooks.js
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

じゃあ多分Fiber.memoizedStateはフックであって対応するフックが1つ以上入るリストだな

Yug (やぐ)Yug (やぐ)

とりあえずworkInProgressHook(現在作業中のフック)をそのcurrentlyRenderingFiber.memoizedStateに代入してるということ。FiberとHookを対応させてる。

ReactFiberHooks.js
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
Yug (やぐ)Yug (やぐ)

このifはファイル内で見つけた最初のuseState、みたいなことか。だからnull

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
}
Yug (やぐ)Yug (やぐ)

で、次else

ReactFiberHooks.js
} else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
}

どういうことだぁぁ...??

なんかhooksの型やfiber.memoizedStateなどがよくわかんなくて混乱してきた...


GPTと問答したらめっちゃなるほどぉぉになったので一応メモ

Fiber.memoizedStateはフックを1つ格納してる。

だがフック自体は単方向リストで次のフックへの参照をHook.nextで保持している

だからFiber.memoizedStateは実質複数のフックを保持していると言えるよね~みたいな話。

なるほどぉぉぉ

こんなイメージ

memo.jsx
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>;
}
memo.js
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の値とかそういうやつ。

これを踏まえて読み直してみよう

Yug (やぐ)Yug (やぐ)
ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)
  • 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への代入は意味無いはず。

うーん、ということで実質これをやってるだけのはずだぞ

ReactFiberHooks.js
workInProgressHook = hook;

つまりすでにcurrentlyRenderingFiber.memoizedStateはあるので、そこへの代入は必要なく、ただこれから作業するという意味でworkInProgressHookにhookを代入(初期化)した感じやな

謎なのでつぶやいといた
https://x.com/clumsy_ug/status/1865744358382911682

Yug (やぐ)Yug (やぐ)

んで最後に、ifでもelseでも関係なくworkInProgressHookを返す

ReactFiberHooks.js
return workInProgressHook;

以上でmountWorkInProgressHook関数はok。

ReactFiberHooks.js
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関数に戻る

Yug (やぐ)Yug (やぐ)

戻ってきた

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

1行目のこれは、プロパティが全部nullの初期化されたworkInProgressHook。

ReactFiberHooks.js
const hook = mountWorkInProgressHook();
Yug (やぐ)Yug (やぐ)

useStateの初期値として受け取ったものが関数であれば、まずその関数をinitialStateInitializerという変数で保存

ReactFiberHooks.js
if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;

関数ということはこれはuseStateのinitializer function(初期化関数)のことだな
https://ja.react.dev/reference/react/useState#parameters

Yug (やぐ)Yug (やぐ)

でその実行結果をinitialStateに代入。initailStateが関数から値になったということ。

ReactFiberHooks.js
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();

んでコメントがあるが、多分flowの型チェックを無視するのではないだろか

initialStateは最初関数だったのにただの値に変わっちゃうのは確かに型エラーが出そうなのでそれを防ぐために。

Reactのリンタ抑制に似てる。// eslint-ignore-next-line react-hooks/exhaustive-depsってやつ
https://ja.react.dev/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize

Yug (やぐ)Yug (やぐ)

次のスコープ

ReactFiberHooks.js
if (shouldDoubleInvokeUserFnsInHooksDEV) {
  setIsStrictModeForDevtools(true);
  try {
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialStateInitializer();
  } finally {
    setIsStrictModeForDevtools(false);
  }
}

shouldDoubleInvokeUserFnsInHooksDEVって何だ?

Yug (やぐ)Yug (やぐ)

それがtrueだったらsetIsStrictModeForDevtools(true);という処理をしていることから、strict modeをtrueにすべきかどうかの情報が入っているはず

直訳しても、「フックス開発環境(?)において、ユーザーの関数を二度呼び出すべきかどうか」なのでStrict Mode関連だろうな

じゃあそのsetIsStrictModeForDevtoolsとは?

これ

packages\react-reconciler\src\ReactFiberDevToolsHook.js
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個見ていくか

Yug (やぐ)Yug (やぐ)

まずconsoleManagedByDevToolsDuringStrictMode

「ストリクトモード時にDevToolsが管理するコンソール」???

定義元はこれ

packages\shared\ReactFeatureFlags.js
export const consoleManagedByDevToolsDuringStrictMode = true;

んでReactFiberDevToolsHook.jsファイル内を見るとconsoleManagedByDevToolsDuringStrictModeに何かしらを代入している式は1つも無いので、true確定

本番環境ではfalseを代入するみたいな処理が他であるんじゃないかと予想

とりあえずdevモードならtrueになるみたいな雑なイメージでいく

Yug (やぐ)Yug (やぐ)

なので今回のif文は通過確定で、その中のスコープはこれ

ReactFiberDevToolsHook.js
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ってなんや?

Yug (やぐ)Yug (やぐ)

これ

packages\react-reconciler\src\Scheduler.js
// 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とは何だろう

Yug (やぐ)Yug (やぐ)

こうimportされてる

Scheduler.js
// 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に切り替えたら、このモジュールを削除できます。

Yug (やぐ)Yug (やぐ)

なるほど、なんとなく理解。
CommonJSのESMラッパーがSchedulerか

Scheduler.js
import * as Scheduler from 'scheduler';

んでそのimport先まで更に辿ってみると、ファイルの中身はこれだけ

packages\scheduler\index.js
'use strict';
export * from './src/forks/Scheduler';

さらにたらい回しか。その先のファイルのexportしてる内容を見にいくとこの4か所

packages\scheduler\src\forks\Scheduler.js
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独自の型が使われてるな。定義されたモジュール内部では普通に使えるけど外部には具体的な型の内容は隠蔽できるみたいなやつらしい。
https://flow.org/en/docs/types/opaque-types/

Yug (やぐ)Yug (やぐ)

ん?でもそれだとlogというプロパティは見当たらないのでこれがおかしいな...?

packages\react-reconciler\src\Scheduler.js
// 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;
Yug (やぐ)Yug (やぐ)

あ、コメントにヒントがあるかも

Scheduler.js
// 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には存在するで、みたいなこと書かれてる

そこ見てみるか

Yug (やぐ)Yug (やぐ)

これだけ

packages\scheduler\unstable_mock.js
'use strict';

export * from './src/forks/SchedulerMock';

そのexport元を見に行くとこれ

Yug (やぐ)Yug (やぐ)

あったぞ!おそらくこれだ

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行見ていくか

Yug (やぐ)Yug (やぐ)

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;
}
Yug (やぐ)Yug (やぐ)

JSでconsole.log.nameと書いてnameの定義元飛んでみたら、こうなってた

lib.es2015.core.d.ts
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であるということやな

Yug (やぐ)Yug (やぐ)

ただそれで言うと、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'に変えられる処理があるのか?

...いや、無かった

https://github.com/search?q=repo%3Afacebook%2Freact+console.log.name+%3D&type=code

じゃあもっと低レイヤーで自動的に切り替えられるみたいな話なのかもしれない。

とにかく、console.log.name === 'disabledLog' になっていたらreturnで強制終了している

それか、disableYieldValueがtruthyであってもreturnで強制終了

disableYieldValueというのは何だ?

「valueを生めない(収穫できない)」かぁ...わからん

Yug (やぐ)Yug (やぐ)

同ファイルに定義があるが、falseが代入されてるだけ

SchedulerMock.js
var disableYieldValue = false;

更新用関数もあるな

SchedulerMock.js
function setDisableYieldValue(newValue: boolean) {
  disableYieldValue = newValue;
}

直接disableYieldValue =という感じで代入されてるとこは無さそうなので、おそらくこの更新用関数で更新されてるはず

んでこの更新用関数は違う名前でexportされてる

SchedulerMock.js
export {
  ...
  setDisableYieldValue as unstable_setDisableYieldValue,

なのでunstable_setDisableYieldValueが使われてる場所を探してみよう

Yug (やぐ)Yug (やぐ)

あった。これだ

packages\react-reconciler\src\ReactFiberDevToolsHook.js
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がどのように使われてるのか探せば良い

Yug (やぐ)Yug (やぐ)

結構使われてるけど、まぁ予想できるように、開発中はtrueにすることで2回レンダーするよ、みたいなことをする必要があるが、そのtrueの部分

packages\react-reconciler\src\ReactFiberHooks.js
if (shouldDoubleRenderDEV) {
  // In development, components are invoked twice to help detect side effects.
  setIsStrictModeForDevtools(true);

なのでdisableYieldValueは実質、「strictModeかどうか≒レンダー2回すべきかどうか」みたいに雑に捕えよう
(でも変数名からはまったく想像できないものになってしまうのでちょっと謎だが...。まぁいい)

理解したので戻るか

Yug (やぐ)Yug (やぐ)

log関数の続きから

packages\scheduler\src\forks\SchedulerMock.js
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にパッチされているということかも。その場合は「レンダリング再生中」とみなして何もしないという感じか?

Yug (やぐ)Yug (やぐ)

んで最後ここ

packages\scheduler\src\forks\SchedulerMock.js
if (yieldedValues === null) {
  yieldedValues = [value];
} else {
  yieldedValues.push(value);
}

yieldedValuesは同ファイルでこう定義されてる。まぁyieldValueが入った配列だろうな

SchedulerMock.js
let yieldedValues: Array<mixed> | null = null;

つまり引数であるvalueを配列内に入れてる(何のためかは知らん)

log関数終了。setIsStrictModeForDevtoolsに戻る

Yug (やぐ)Yug (やぐ)
packages\react-reconciler\src\ReactFiberDevToolsHook.js
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はテストだと考えて良さそう。それ以外は意味わからん。

Yug (やぐ)Yug (やぐ)

んでこれ

unstable_setDisableYieldValue(newIsStrictMode);

さっき見た通りこれは実質setDisableYieldValueで、中身はこれ

SchedulerMock.js
function setDisableYieldValue(newValue: boolean) {
  disableYieldValue = newValue;
}

つまり引数をdisableYieldValueというstrict modeかどうかみたいな謎の変数に代入するだけ

Yug (やぐ)Yug (やぐ)

んで次これ。ここからがまだ見てない。

setSuppressWarning(newIsStrictMode);

定義元はこれ

packages\shared\consoleWithStackDev.js
// 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.
}

意味無さそうやな。とばす

Yug (やぐ)Yug (やぐ)

setIsStrictModeForDevtools関数内の残りは見る意味薄そうだし飛ばすか。
まぁ要は、実質strictmodeにする(ためにいろいろやる)関数って感じかな(知らん)

mountStateImplに戻る

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

mountWorkInProgressHookって何だったか忘れたので見直した

まぁ要はfiberが持ってるhookの連結リストを初期化する、もしくは追加していく、てことやね

とはいえそのhookは全プロパティがnullの初期化されたものなのは同じ。

んで最後にその初期状態のhookをreturnして終わり。

(何なら循環リストか?)
https://x.com/calloc134/status/1861254365543309792

Yug (やぐ)Yug (やぐ)

なのでその初期状態hookをhookとして取得。

次のスコープ

ReactFiberHooks.js
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);
    }
  }
}
Yug (やぐ)Yug (やぐ)

更新用関数を実行するとこまではもうやったので割愛。

んでstrict modeのような2度実行が必要だったらstrict modeをtrueにする

んで2度目の更新用関数実行

最後にstrict mode(による2度実行の情報?)をオフ

Yug (やぐ)Yug (やぐ)

最後のスコープ

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に代入

Yug (やぐ)Yug (やぐ)

てかHook.baseStateってなんだ?

GPTに聞いたらなるほどぉになったのでメモ

まぁbaseStateがスナップショットつまり過去の値で、差分比較のために使われるbaseとしてのstateみたいな感じっぽいな

んでmemoizedStateは常に更新される最新のstate

Yug (やぐ)Yug (やぐ)

次の行、UpdateQueueってなんだ?

同ファイルに定義ある

ReactFiberHooks.js
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型だったけど、どっかで繋がってるんじゃないかなぁという妄想
https://zenn.dev/link/comments/1ea92c3ab4da50

Yug (やぐ)Yug (やぐ)

とりあえずUpdateオブジェクトをpendingプロパティとして持っているな

Updateは同ファイルにある、おなじみのこれ

ReactFiberHooks.js
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

S, AはState, Actionだろうな

Yug (やぐ)Yug (やぐ)

まぁ要はupdateQueueはupdateオブジェクトやその他もろもろを格納してる

んでupdate自体は連結リストになってる

Yug (やぐ)Yug (やぐ)

復習。BasicStateActionは懐かしのこれ

ReactFiberHooks.js
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;

つまりsetStateの引数

ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

setStateそのものがDispatch<BasicStateAction<S>>

Yug (やぐ)Yug (やぐ)

ということで戻ると、

ReactFiberHooks.js
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は入れてあげてるので完全初期状態ではないって感じ

Yug (やぐ)Yug (やぐ)

んで.memoizedState以外はnullのままの半分初期状態みたいな感じのhookのqueueプロパティにさっきのinitialStateを反映したqueueを代入してあげる

でそのhookをreturnして終了

Yug (やぐ)Yug (やぐ)

なのでmountStateImplをまとめると...

  • mountWorkInProgressHookにより、fiberが持ってるhookの連結リストを初期化/追加
  • 更新用関数だったら2度実行、その後strictmode(の情報?)をオフに
  • 最終的なinitialStateをhookに反映させる
  • 新しいinitialStateを反映したqueueもhook.queueに代入
  • 最後に結果としてできあがったhookをreturn

まぁ要はinitialStateをhookに反映させて、そのhookを返却するよ~ていう関数やね

Yug (やぐ)Yug (やぐ)

よっしゃ、mountStateImplが終わったのでやっとmountStateに戻れる、戻ろう

ReactFiberHooks.js
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];
}
Yug (やぐ)Yug (やぐ)

ふむふむ

  • const hook = mountStateImpl(initialState);でinitialStateを反映したhookを取得

  • そのhookの.queueをqueueという変数名で取得

  • dispatchという関数を作る

    • 型がDispatch<BasicStateAction<S>>であることからこのdispatchというのはsetState関数そのものであることがわかる
Yug (やぐ)Yug (やぐ)

だが代入の内容が複雑だな、言語化したい

const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
  null,
  currentlyRenderingFiber,
  queue,
): any);

あーbindか、前見たなぁ

ん、これか
https://zenn.dev/link/comments/3029716222f518

じゃあdispatchSetStateというのが何なのかさえわかれば良さそうだな

Yug (やぐ)Yug (やぐ)

同ファイルにあった。これ

ReactFiberHooks.js
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);
}
Yug (やぐ)Yug (やぐ)

arguments使ってるな
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/arguments

なるほど、bindしてるから呼び出されるときは引数省略されるとはいえ、dispatchSetStateとしてはすでに引数3つまで読んでることになってて、プラス1個つまりarguments[3]があった場合は、それはsetStateの引数に第二引数が呼ばれたということで、それはおかしいのでエラーを出すという感じだろうな

Yug (やぐ)Yug (やぐ)

次、const lane = requestUpdateLane(fiber);のrequestUpdateLaneとはなんだ

定義元これ

packages\react-reconciler\src\ReactFiberWorkLoop.js
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を受け取ってレーン(優先度)を返す

Yug (やぐ)Yug (やぐ)

じゃあもしかしてtransitionと遅延ローディング(lazy loading)ってやつ同じか?

Yug (やぐ)Yug (やぐ)

すごい長いコメントがある。直訳するとこうなる

これはレンダーフェーズのアップデートです。これらは公式にはサポートされていません。古い動作は、現在レンダリングしているものと同じ「スレッド」(レーン)を与えることです。そのため、同じレンダリングの後半で発生するコンポーネントで setState を呼び出すと、フラッシュされます。理想的には、特殊なケースを取り除き、インターリーブされたイベントから来たかのように扱いたい。いずれにせよ、このパターンは公式にはサポートされていません。この動作はフォールバックに過ぎない。既存のコードが誤って現在の動作に依存してしまう可能性があるため、setState警告をロールアウトできるようになるまで、このフラグは存在するだけです。

全然意味わからん。ちなみにインターリーブは「割り込む or 構成要素を入れ替える/交互に配置する」みたいな意味っぽい

https://ejje.weblio.jp/content/interleave#goog_rewarded

Yug (やぐ)Yug (やぐ)

うーん細かくは見ないことにするがとりあえず、引数で受け取ったfiberに適切なLaneを計算して返す、みたいな関数だろうな

前見たように、Fiberにも.lanesというプロパティがあるので

Yug (やぐ)Yug (やぐ)

dispatchSetStateに戻る

ReactFiberHooks.js
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);
}
Yug (やぐ)Yug (やぐ)

次、dispatchSetStateInternalとは何だ?

すぐ下に定義があった

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

fiber, queue, action. laneを受け取ってbooleanだけを返すのか

とりあえず最初のisRenderPhaseUpdateとenqueueRenderPhaseUpdateを見てみる

同ファイルに定義がある

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

isRenderPhaseUpdateはfiberを受け取って、そのfiberがrender中のものかどうかをbooleanで返す

enqueueRenderPhaseUpdateはqueue, updateを受け取って、updateオブジェクトの循環リストを作っていく感じ

Yug (やぐ)Yug (やぐ)

GPT

たしかにrender中にsetStateが呼ばれたみたいなときにここを通過するのかも。
だから特別な処理が行われるのも納得できる

Yug (やぐ)Yug (やぐ)

つまりこの部分は、「render中だったらそのupdateを循環リストに突っ込んどく」みたいなことをしてる感じか

if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
}

じゃあレンダー中じゃなかったらどうするの?すぐ更新を反映するみたいなこと?

ということでelse下を見ていく

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

というか更新はレンダー中ではないのが普通よな。
ボタンを連続でクリックしまくるみたいな時にのみrender中になってしまう可能性がある程度だと思うので。そんなの特殊ケースな気がする

Yug (やぐ)Yug (やぐ)

fiberもしくはalternateのlanesがNoLanesだった時のみ通過する ←??

if (
  fiber.lanes === NoLanes &&
  (alternate === null || alternate.lanes === NoLanes)
)

ちなみにNoLaneはレーンの中で一番優先度が高いデフォルト値

特殊なレーンとして「NoLane」という全てのビットが0がレーンがあります。
これはJavaScriptでいう所のundefinedのような、レーンを何も設定していない際のデフォルト値であり、最も高い優先度です。(もっとも、「NoLane」を優先度として使うことは稀です)

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#レーンは優先度を表す

んー、てことはfiberができたてホヤホヤだからレーンもデフォルト値のままで、その場合にのみ処理をしていきたい、みたいなことなのだろうか

Yug (やぐ)Yug (やぐ)
// 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をスキップするという意味のドタキャンでは無いだろうか。「新しい状態が現在の状態と同じなら」って言ってるし
https://eigolab.net/212

それを頭の隅に入れた状態で読み進めていく

Yug (やぐ)Yug (やぐ)
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 });
  // ...

https://ja.react.dev/reference/react/useReducer#usereducer

おそらく、スナップショットとしてのreducer、つまり直近で使われてた古いreducer的な意味合いか?

Yug (やぐ)Yug (やぐ)
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型のやつ↓

packages\react\src\ReactSharedInternalsClient.js
export type SharedStateClient = {
  H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
  ...

const ReactSharedInternals: SharedStateClient = ({
  H: null,
  ...

export default ReactSharedInternals;
  • ReactSharedInternals.HにInvalidNestedHooksDispatcherOnUpdateInDEVを代入。
    InvalidNestedHooksDispatcherOnUpdateInDEVってやつもDispatcherか。
ReactFiberHooks.js
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
Yug (やぐ)Yug (やぐ)

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()はこれ

ReactFiberHooks.js
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という名前の通り。

Yug (やぐ)Yug (やぐ)

うーんなんでlastRenderedReducerが存在するかつ__DEV__だったら無効なフックになるのかわからんが、次にいくか

Yug (やぐ)Yug (やぐ)
ReactFiberHooks.js
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と同じじゃね...?使い分けが気になるが一旦無視
https://zenn.dev/link/comments/9ecea1d72a042e

Yug (やぐ)Yug (やぐ)

とりあえず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,
};
Yug (やぐ)Yug (やぐ)

なのでその古いreducer関数を実行してることになる

const eagerState = lastRenderedReducer(currentState, action);

引数はcurrentState(古い直近のstate)とaction。その返り値がeagerStateとして代入される。

なるほど、たしかにreducerは更新処理を実行し、結果としてのstateを返すのでそれをeagerStateとして取得する訳か

Yug (やぐ)Yug (やぐ)

// 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って「へそくり」とか出てくるから意味をよくわかっていなかったんだけど、「格納」って意味なんだ

Yug (やぐ)Yug (やぐ)

そんなことしてなくない?stateを入れてるだけでreducerは入れてないように見えるが...一旦飛ばす

熱心に計算されたstateと、それを計算するために使用されたreducerをupdateオブジェクトに格納します

Yug (やぐ)Yug (やぐ)

ReactFiberHooks.js
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じゃないのか?

Yug (やぐ)Yug (やぐ)

定義元ファイル、これ

packages\shared\objectIs.js
/**
 * 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 によって行われます

https://ja.react.dev/reference/react/useState#setstate-caveats

Object.isだけじゃなくて、うまく拡張してる感じか

Yug (やぐ)Yug (やぐ)

今回、isという名前でimportしてるが、

ReactFiberHooks.js
import is from 'shared/objectIs';

実態としてはobjectIs関数なので、「isという名前でObject.isを使う慣習がある」と把握しておいても良さそうやな

objectIs.js
export default objectIs;
Yug (やぐ)Yug (やぐ)

コメント直訳

Object.isポリフィルをインライン化し、コンシューマが独自に出荷する必要がないようにした。

ポリフィルとは互換性保つためのコード的な感じか
https://developer.mozilla.org/ja/docs/Glossary/Polyfill

Object.isの型が関数だったら普通にObject.is使うけど、もし関数じゃなかったらisという独自関数を使ってObject.isの代わりにするみたいな感じなのかな。

関数じゃない場合と言うのは、そうなってしまうReactユーザの環境?が多分あるんだろうな。

とりあえず、is関数というポリフィル(互換関数)も用意してあるよ~という感じ。

GPT
なるほど、Object.isはES6からなのか

Object.is は ES6(ECMAScript 2015)で導入されたため、古いブラウザや環境では利用できない場合があります。

  • 環境が Object.is をサポートしている場合にのみ、そのまま利用します。
  • もしサポートされていない場合は、is 関数というポリフィル(互換関数)を代わりに使用します。

なるほど

例えば、ES5以前の環境では Object.is が存在しません。

Yug (やぐ)Yug (やぐ)

ということで戻る

ReactFiberHooks.js
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つを比較すると言える。

  1. currentState = 直近の古いstate
  2. eagerState = 直近の古いreducerを使っているがその引数のactionは新しいやつで、その実行結果としてのstate
    • actionはbindされた結果としてのdispatch関数(実質setState関数)の第一引数に渡されるものなので、新しく渡されたものと考えて良いはず(mountState関数から辿ればそう判断できる)
Yug (やぐ)Yug (やぐ)
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で問いかけてんのは何?誰に対しての問いかけ?そしてどういう意味?
Yug (やぐ)Yug (やぐ)

ていうかふと思ったことメモ:

直近のreducerがあった場合のみif通過してきてるけど、useReducerはまったく使わずにただuseStateだけ使ってた場合はどうなるんだ?そうなるとreducerなんて存在しないはずだと思うんだが。

reducerに比重を置きまくってるのが違和感。確かにuseReducerもstate更新に関わるとはいえ。

Yug (やぐ)Yug (やぐ)

あとdispatchSetStateInternalの返り値であるtrue/falseは何の真偽値なのかも確認したい

一応GPTメモ

Yug (やぐ)Yug (やぐ)

GPT

そりゃそうじゃね?としか思わないというか、よくわからない...

Yug (やぐ)Yug (やぐ)

なるほど、確かにソースコードのコメントなんだからReact開発者へのメッセージか。

このスキップする処理がtransitionに関係するなら、考えないといけないことがあるかもよ、みたいなことか。transition自体よくわからんのでイメージできないが、とりあえずok

Yug (やぐ)Yug (やぐ)

てことはこのenqueueConcurrentHookUpdateAndEagerlyBailout関数はrenderをスキップする処理なんじゃなかろうか。見てみる

ReactFiberHooks.js
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;
}

これ

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js
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();
  }
}
Yug (やぐ)Yug (やぐ)

引数の型で使われてるHookQueueとHookUpdateってなんや?て思ったけどなぜかasで名前変えてimportしてるだけで、実態はおなじみUpdateQueueとUpdateのこと

ReactFiberConcurrentUpdates.js
import type {
  UpdateQueue as HookQueue,
  Update as HookUpdate,
} from './ReactFiberHooks';
Yug (やぐ)Yug (やぐ)
// 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は必要だ、みたいな感じ?わからん。飛ばす

Yug (やぐ)Yug (やぐ)
ReactFiberConcurrentUpdates.js
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);

laneをNoLaneで初期化。

ConcurrentQueueとConcurrentUpdateとは?さっきと同じでUpdateQueueとUpdateのことか?

→違った。独自に作られたやつか。UpdateQueueとUpdateを簡易化したというか、不要なプロパティをなくした版みたいな感じか

ReactFiberConcurrentUpdates.js
export type ConcurrentUpdate = {
  next: ConcurrentUpdate,
  lane: Lane,
};

type ConcurrentQueue = {
  pending: ConcurrentUpdate | null,
};

UpdateQueueとUpdateはこれ。再掲

ReactFiberHooks.js
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,
};
Yug (やぐ)Yug (やぐ)

でもそれだと代入できないはずでは?

ReactFiberConcurrentUpdates.js
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);

代入されてるqueueとupdateは引数で渡ってきたものであって、それは実質UpdateQueueとUpdateだからなぁ。ConcurrentQueueとConcurrentUpdateには適合できないはず

ReactFiberConcurrentUpdates.js
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
): void {

うーん、flowでは一致したプロパティがあるなら強制的に型変換することが可能なのか?

とりあえずそう理解する(しかないような)

Yug (やぐ)Yug (やぐ)

ということで次

ReactFiberConcurrentUpdates.js
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);

enqueueUpdateはこれ

ReactFiberConcurrentUpdates.js
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はこれ

ReactFiberConcurrentUpdates.js
// 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;
Yug (やぐ)Yug (やぐ)

レンダーが進行中であり、並行イベントからupdateを受け取った場合、現在のレンダリングが完了するか中断されるまで、updateをfiber/hookキューに追加するのを待ちます。
この配列に追加しておき、後でキューや fiber、updateなどにアクセスできるようにします。

ふむふむ、updateを受け取ったとしてもそれがrender中だったら、そのupdateをfiber/hookのqueueに入れたくないので、このconcurrentQueuesに入れておくよ、みたいな感じか

確かに前見た通りFiber.updateQueueHook.baseQueueもupdateの循環リストなので、そこに直接入れないよ、てことね

https://zenn.dev/link/comments/6cdf49fc10ccaf
https://zenn.dev/link/comments/a33a17eca75668

へぇ、てことは直接fiber/hookのキューに入れるupdateはrender中では無いやつのみなんだ。

とはいえrenderが終わったらrender中のupdate関連であるfiber, update, キューとかにアクセスできるようにするために、concurrentQueuesにそれらの情報をpushしておくのね。

へぇぇ、なんかいろいろ考えられててすごいなぁ

Yug (やぐ)Yug (やぐ)

...ただ、そうなると矛盾が生じないか?

今見てるコードの大元はこのdispatchSetStateInternalな訳だが、

ReactFiberHooks.js
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中の更新ではないことが確定しているはず。

ReactFiberHooks.js
if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
} else {

render中ではないスコープなのに、そこでrender中のupdateのためのenqueue処理をしているということよね。これっておかしくないか???

Yug (やぐ)Yug (やぐ)

メモ:render中に更新するというのはコンポーネントのトップレベルでsetState実行するのと同じはずなので、それによる無限ループというあの謎を解明することに繋がるかもしれない...
(あれ、0を0に変更するのでも無限ループするのが謎なんだよな...)

function MyComponent() {
  const [count, setCount] = useState(0);

  // render中に状態更新を行う
  setCount(count + 1); // NG: render中に状態更新を発生させている
  return <div>{count}</div>;
}

ということで今度enqueueRenderPhaseUpdateを解明したいな

Yug (やぐ)Yug (やぐ)

ふーむ、GPTに聞いたらこんな感じだった

  1. isRenderPhaseUpdate(fiber)がfalseであることは、確かに「現在のfiberがrenderフェーズの更新対象でない」ことを意味する
  2. だが、Reactアプリ全体では「並行的なレンダリング」が進行している可能性があり、その場合レンダリング中のfiber以外から並行して更新が発生し得る
  3. concurrentQueuesはその並行的に発生した更新を一時的に溜めておく役割(後続で安全に処理する)
  4. つまりrender中でなくても並行して別のレンダリングやイベント処理が発生する可能性があり、そういったものをカバーするのがconcurrentQueuesである

疑問

  1. 並行レンダリングとは?まさかレンダーって並行処理されてんの?
    • レンダーなのかレンダリングなのか
  2. render中でなくても並行レンダーによるupdateをconcurrentQueuesが受け取るのはわかったが、render中のupdateも受け取るという認識で良い?コードのコメントを見る限りそうだと思うんだが
Yug (やぐ)Yug (やぐ)

戻ってきた。並行レンダーというのはuseTransitionの概念とほぼイコールな気がしている。

優先度高い処理をレンダーとして進めながら、transition内の優先度低い処理もバックグラウンドでレンダーとして(?)進めるみたいな処理のことだと思われる

厳密に「並行処理」と言えるのかどうか、つまり1つのコアで高速に複数プロセスを切り替えているのかどうかというと、それはNOだという可能性もあるが、まぁそういうことを仮想的に実現はできているよということなのかもしれない。

Yug (やぐ)Yug (やぐ)

てか今思ったが、並行というのが厳密にはどうなっているのかというのは、useTransitionの内部実装を理解すれば理解できる気がする。transitionの優先度低い処理がどのように中断され、どのように再開されるのか、その際にレンダーは2つあって(=並行みたいな)、主レンダーと副レンダー(バックグラウンド処理)みたいになっているのか、など

Yug (やぐ)Yug (やぐ)

疑問2:render中でなくても並行レンダーによるupdateをconcurrentQueuesが受け取るのはわかったが、render中のupdateも受け取る?

いや、多分それは無いと思うんだよな。

なぜならそれはdispatchSetStateInternal関数内の以下でenqueueされるもののはずなので。

ReactFiberHooks.js
if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
}

ただ、そうなると引っかかるのが以下のコメント。

ReactFiberConcurrentUpdates.js
// 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> = [];

さっき訳した通りこうなる

レンダーが進行中であり、並行イベントからupdateを受け取った場合、現在のレンダリングが完了するか中断されるまで、updateをfiber/hookキューに追加するのを待ちます。
この配列に追加しておき、後でキューや fiber、updateなどにアクセスできるようにします。

「レンダーが進行中であり」ってとこがめっちゃ引っかかる。

なので自分なりに「レンダーが進行中であり、並行イベントからupdateを受け取った場合」を以下のように解釈しようと思う。

現在のfiberはレンダー中でないのは確定しているが、バックグラウンドで別のレンダーが走っており(並行レンダーによる)、その並行レンダーからupdateを受け取った場合

そして...

ここで言うレンダーというのは現fiberのレンダーではなくバックグランドのレンダー
もしくは現fiberが実はそもそもバックグランド処理中のもので、その逆としてのメイン側のレンダーのことの可能性もあるな。

まぁともかく今のレンダーではない方の別の(並行)レンダーということだと思われる。

Yug (やぐ)Yug (やぐ)

ということでenqueueUpdateに戻る

ReactFiberConcurrentUpdates.js
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);
  }
}
Yug (やぐ)Yug (やぐ)

あーconcurrentQueuesにfiber, queue, update, laneの順番で突っ込むんか

まじで種類とか関係なく突っ込むんだなぁ、すごい雑な気がするけどそれで機能するのか、へぇ

んで、今ってObject.isがtrueだったスコープ内なので、並行とか関係なくただ値が同じだった更新、てことになると思うんだが、それでもconcurrentQueuesが使われてるってことは...

値に変更が見られない無駄な更新も並行レンダーとして扱うと理解しても良いかもしれない!

Yug (やぐ)Yug (やぐ)

んで次

ReactFiberConcurrentUpdates.js
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

mergeLanesはこれ

packages\react-reconciler\src\ReactFiberLane.js
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

2つのLaneもしくはLanesを受け取り、それらの論理和としてのLanesを返す

ビット単位の論理和なのでどちらかが1だったらそこは1になるって感じだろうな。なので確実にその数以上になる。減少はしない。

んで、レーンは数が大きければその分優先度が低くなるので、このmergeLanes関数は受け取ったレーンよりも低いレーンを返すことになるな。
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#レーンは優先度を表す

Yug (やぐ)Yug (やぐ)

いや、そういう訳でもないか。数が大きければ優先度が低いと一概に言えないかも。

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#レーンは優先度を表す

const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;

// `|`でレーンをマージ
// `SyncLane`と`InputContinuousLane`を持つビットマスクになる
const merged = SyncLane | InputContinuousLane; 
console.log(merged); // 0b0000000000000000000000000000101

// `&`でビットマスクが対象のレーンを含むか確認できる
console.log((merged & InputContinuousLane) != 0); // true

「両方のレーンを持つ状態になる」と言った方が適切かも

Yug (やぐ)Yug (やぐ)

でもそれってどういう状態やねん?複数のレーンが混ざっちゃったら優先度はどうなるんだ?

あー、なんとなくわかったかも。

あるタスクがあって、その中に高優先度のプログラムと低優先度のプログラムが内包されている場合、そのタスクに対して複合レーンが割り当てられるのではないだろか。

タスクの中で先に高優先度のものだけレーンを参考にしながら処理していく、みたいな流れ

これはありそう。

Yug (やぐ)Yug (やぐ)

戻る

ReactFiberConcurrentUpdates.js
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

concurrentlyUpdatedLanesは最初、NoLanesとして初期化されてる。だがその後代入されたらNoLanesではなくなることもあるという感じだろう。

ReactFiberConcurrentUpdates.js
let concurrentlyUpdatedLanes: Lanes = NoLanes;

なのでそれと、引数で受け取ったlaneをmergeしたものを新しいconcurrentlyUpdatedLanesにしているということ。

ちなみにその「引数で受け取ったlane」はNoLane確定。呼び出し元を見ればわかる

ReactFiberConcurrentUpdates.js
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);
Yug (やぐ)Yug (やぐ)

ReactFiberConcurrentUpdates.js
// 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);

コメントの訳

fiberのlaneフィールドは、いくつかの場所で「何らかの作業がスケジュールされているか」をチェックするためや、早期に処理をスキップ(bailout)するために使用されるため、すぐに更新する必要があります。
TODO: この処理を「shared(共有)」キューに移動すべきかもしれません。

レンダーの早期スキップ(eager bailout)を表現するためにfiber.lanesを更新

第二引数のlaneはNoLaneなので、NoLaneとmergeしたものを新しいfiber.lanesにしてることになる

優先度を高めて、早くスキップ作業を終わらせたいってことかもしれない

Yug (やぐ)Yug (やぐ)

最後

ReactFiberConcurrentUpdates.js
const alternate = fiber.alternate;
if (alternate !== null) {
  alternate.lanes = mergeLanes(alternate.lanes, lane);
}

fiber.alternateは、「前のコミットフェーズでDOMにコミットした古いファイバーへのリンク」
つまり「差分検出用の古いfiber(へのリンク)」
https://zenn.dev/link/comments/62f900f250fa98
https://zenn.dev/link/comments/05f84f1fd5e921

その古いfiberがnullでなかったらif通過して以下実行

alternate.lanes = mergeLanes(alternate.lanes, lane);

alternate.lanesとlane(実質NoLane)をmergeして、それを新しいalternate.lanesにしてる

てことは、新しいfiberも古いfiberも、レーンの更新を済ませちゃうって感じか。ちゃんと辻褄合わせるために。

eagerだけにどんどん処理を終わらせていくのね

Yug (やぐ)Yug (やぐ)

ということでenqueueUpdateをまとめると...

ReactFiberConcurrentUpdates.js
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にfiber, queue, update, laneをごちゃ混ぜ四兄弟として種類関係なく突っ込む
    • concurrentQueuesは並行レンダーによるupdateを受け取るところ
      • 多分主レンダーに対する副レンダー的な扱い
  • concurrentlyUpdatedLanesとfiber.lanesとalternate.lanesを、NoLaneとmergeした結果として更新
  • なのでenqueueUpdateという名前のenqueueは、concurrentQueuesに対するenqueueだな
    • んでupdateはfiber, queue, update, laneのこと(ごちゃ混ぜ四兄弟)
Yug (やぐ)Yug (やぐ)

呼び出し元に戻る

ReactFiberConcurrentUpdates.js
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();
  }
}

enqueueUpdate呼び出しの後から見ていく

Yug (やぐ)Yug (やぐ)
// 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).

通常は、同時進行中の(並行)キューを処理するために、次のレンダーフェーズに頼ることができる。しかし、これは救済措置であるため、ここでは作業をスケジューリングしていない。そのため、キューに入れたばかりのアップデートは、何か別の作業がスケジュールされるまで(仮にあったとしても)リークすることになる。

リークするというのがよくわからん。漏洩?キュー内のアップデートが漏洩するってどゆこと?

「何も処理されずにただ格納されたまんまで置いてけぼりになる」とかだったら理解できるが。

Yug (やぐ)Yug (やぐ)

ReactFiberConcurrentUpdates.js
// 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;

現在ツリーをレンダリングしている最中かどうかをチェックし、もしそうでなければ、漏れを防ぐためにキューを即座に処理する。

getWorkInProgressRootはこれ

packages\react-reconciler\src\ReactFiberWorkLoop.js
export function getWorkInProgressRoot(): FiberRoot | null {
  return workInProgressRoot;
}

workInProgressRootはこれ

ReactFiberWorkLoop.js
let workInProgressRoot: FiberRoot | null = null;

その型であるFiberRootはこれ

packages\react-reconciler\src\ReactInternalTypes.js
// Exported FiberRoot type includes all properties,
// To avoid requiring potentially error-prone :any casts throughout the project.
// The types are defined separately within this file to ensure they stay in sync.
export type FiberRoot = {
  ...BaseFiberRootProperties,
  ...SuspenseCallbackOnlyFiberRootProperties,
  ...UpdaterTrackingOnlyFiberRootProperties,
  ...TransitionTracingOnlyFiberRootProperties,
  ...ProfilerCommitHooksOnlyFiberRootProperties,
};

Exported FiberRoot 型にはすべてのプロパティが含まれています。
プロジェクト全体で潜在的にエラーを引き起こす可能性のある :any キャストを必要としないようにするためです。
型はこのファイル内で個別に定義されており、同期が保たれるようにしています。

FiberRootNodeのことじゃないかな
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#fiberrootnode

FiberRootNode

「FiberNode」とは異なるオブジェクト構造をしています。このノードは最終的な表示先(今回はdiv#root)を管理しており、containerInfoというプロパティでdiv#rootにアクセスできます。

Yug (やぐ)Yug (やぐ)

お、やっぱそうだな。中身の1つを見てみたらcontainerInfoがある

ReactInternalTypes.js
type BaseFiberRootProperties = {
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,

  // Any additional information from the host associated with this root.
  containerInfo: Container,
  ...

おそらくこのcontainerが<div id="root">みたいなルートDOM要素への参照のはず
(それを見つけられなかったので今回は飛ばすが)

Yug (やぐ)Yug (やぐ)

なのでgetWorkInProgressRootはただFiberRootNodeを返す関数

export function getWorkInProgressRoot(): FiberRoot | null {
  return workInProgressRoot;
}

FiberRootNodeが存在しなかったらnullになるのか

存在しない時というのが、レンダー中でない時、ということ?

Yug (やぐ)Yug (やぐ)

それを踏まえて戻る

// 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;

現在ツリーをレンダリングしている最中かどうかをチェックし、もしそうでなければ、漏れを防ぐためにキューを即座に処理する。

んー、FiberRootNodeが作られるのはroot.renderではなくReactDOM.createRoot(現jsxs/jsx)なので、レンダーとは関係ない気が...
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#★1-「fiberrootnode」を生成

まぁでもcreateRootが呼び出されるのはレンダー中な訳だし、だったらFiberRootNodeがあるかどうかで、レンダー中かどうかを確認できると言っても良いよねーみたいな感じ?かも

ということで、getWorkInProgressRoot()の結果が

  • nullだったら

FiberRootNodeが無い、つまり並行レンダーは行われていないという判断でisConcurrentlyRendering変数にfalseが入る

  • nullでなかったら

FiberRootNodeがある、つまり並行レンダーは行われているという判断でisConcurrentlyRendering変数にtrueが入る

Yug (やぐ)Yug (やぐ)

if (!isConcurrentlyRendering) {
  finishQueueingConcurrentUpdates();
}

ここが「もしそうでなければ、漏れを防ぐためにキューを即座に処理する」の部分か

つまり並行レンダーが行われていない(FiberRootNodeがない)なら、並行アップデートのキューイングをfinishしてしまうみたいな感じっぽい

じゃあそのfinishとはなんぞやというと、これ

ReactFiberConcurrentUpdates.js
export function finishQueueingConcurrentUpdates(): void {
  const endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;

  concurrentlyUpdatedLanes = NoLanes;

  let i = 0;
  while (i < endIndex) {
    const fiber: Fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const queue: ConcurrentQueue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const update: ConcurrentUpdate = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const lane: Lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    if (queue !== null && update !== null) {
      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;
    }

    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}
Yug (やぐ)Yug (やぐ)

まずここから

const endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0;

concurrentlyUpdatedLanes = NoLanes;
  • 終了時点のindexをendIndexという変数で保存
  • それが終わったらconcurrentQueuesIndexを0で初期化
  • concurrentlyUpdatedLanesもNoLanesで初期化
Yug (やぐ)Yug (やぐ)

const fiber: Fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
const queue: ConcurrentQueue = concurrentQueues[i];
concurrentQueues[i++] = null;
const update: ConcurrentUpdate = concurrentQueues[i];
concurrentQueues[i++] = null;
const lane: Lane = concurrentQueues[i];
concurrentQueues[i++] = null;

あー、さっきのenqueueUpdate内のごちゃまぜ雑だなとは思ってたが、ちゃんとfiber->queue->update->laneっていう順番は決められてるみたい。まぁさすがにそうよね

んでそれぞれ受け取っていって、受け取った後はnullで初期化。

Yug (やぐ)Yug (やぐ)

if (queue !== null && update !== null) {
  const pending = queue.pending;

queueとupdateがnullではなく存在したらif通過

queueはConcurrentQueue型。queue.pendingはConcurrentUpdate型で、それはUpdateの簡易版みたいなやつ

export type ConcurrentUpdate = {
  next: ConcurrentUpdate,
  lane: Lane,
};

type ConcurrentQueue = {
  pending: ConcurrentUpdate | null,
};

つまりその簡易版updateをpendingという変数に保存している

Yug (やぐ)Yug (やぐ)

if (pending === null) {
  // This is the first update. Create a circular list.
  update.next = update;
} else {

その簡易版updateであるqueue.pendingがnullだったら、updateをupdate.nextに代入。

これが最初の更新。循環リストを作成する。

finishということでupdate自体が最後でいいはずだが、更にそのupdate.nextに同じupdateを代入するのはよくわからん

Yug (やぐ)Yug (やぐ)

if (pending === null) {
} else {
  update.next = pending.next;
  pending.next = update;
}
queue.pending = update;

queue.pendingであるupdateが存在したらelseに入る

  • pending.nextをupdate.nextに代入
  • updateをpending.nextに代入
    • pending.nextも最新情報を常に反映するという感じか
  • 最後に、if/else関係なくupdateをqueue.pendingに代入
    • さっきconst pending = queue.pendingとわざわざ代入したんだから、pending = updateとすればいいのに。謎
Yug (やぐ)Yug (やぐ)

最後

if (lane !== NoLane) {
  markUpdateLaneFromFiberToRoot(fiber, update, lane);
}

ループ内で受け取ったlaneがNoLaneではなかったらmarkUpdateLaneFromFiberToRootとやらを呼び出してる

関数名だけだと意味わからんので見ていく

Yug (やぐ)Yug (やぐ)

markUpdateLaneFromFiberToRootはこれ

ReactFiberConcurrentUpdates.js
function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  update: ConcurrentUpdate | null,
  lane: Lane,
): void {
  // Update the source fiber's lanes
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
  // Walk the parent path to the root and update the child lanes.
  let isHidden = false;
  let parent = sourceFiber.return;
  let node = sourceFiber;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }

    if (parent.tag === OffscreenComponent) {
      // Check if this offscreen boundary is currently hidden.
      //
      // The instance may be null if the Offscreen parent was unmounted. Usually
      // the parent wouldn't be reachable in that case because we disconnect
      // fibers from the tree when they are deleted. However, there's a weird
      // edge case where setState is called on a fiber that was interrupted
      // before it ever mounted. Because it never mounts, it also never gets
      // deleted. Because it never gets deleted, its return pointer never gets
      // disconnected. Which means it may be attached to a deleted Offscreen
      // parent node. (This discovery suggests it may be better for memory usage
      // if we don't attach the `return` pointer until the commit phase, though
      // in order to do that we'd need some other way to track the return
      // pointer during the initial render, like on the stack.)
      //
      // This case is always accompanied by a warning, but we still need to
      // account for it. (There may be other cases that we haven't discovered,
      // too.)
      const offscreenInstance: OffscreenInstance | null = parent.stateNode;
      if (
        offscreenInstance !== null &&
        !(offscreenInstance._visibility & OffscreenVisible)
      ) {
        isHidden = true;
      }
    }

    node = parent;
    parent = parent.return;
  }

  if (isHidden && update !== null && node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    markHiddenUpdate(root, update, lane);
  }
}
Yug (やぐ)Yug (やぐ)

とりあえず子、親、alternateのレーンを更新してるっぽい

んでもし親fiberのtagプロパティがOffscreenComponentなら、長いコメントに入る

このオフスクリーンの境界が現在隠されているかどうかをチェックする。

Offscreenの親がアンマウントされた場合、インスタンスはNULLになる可能性があります。通常、親が削除されるとツリーからファイバーを切り離すため、その場合は親に到達できません。しかし、マウントされる前に中断されたファイバーで setState が呼び出される奇妙なエッジケースがあります。このファイバーはマウントされないので、削除されることもない。削除されないので、リターン・ポインタが切断されることもない。つまり、削除されたOffscreenの親ノードに接続されている可能性があるということです。(この発見から、もしコミット・フェーズまでreturnポインターをアタッチしないなら、メモリ使用量の点で良いかもしれないということがわかった。しかしそうするためには、スタック上など、最初のレンダー中にリターン・ポインタを追跡する他の方法が必要だ。)

このケースは常に警告を伴うが、それでも我々はそれを考慮する必要がある。(私たちが発見していない他のケースもあるかもしれない)

ようわからん。

ちなみにOffscreenはまだ開発中の新機能(現Activity)だと思う。なのでそんな重要ではなさそう。
https://zenn.dev/link/comments/f2d08920025e9b

もしOffscreenだったらisHidden = true;にするだけ

Yug (やぐ)Yug (やぐ)

node = parent;
parent = parent.return;

nodeを親に、parentを親の親にする。トップになるまでずっと上がっていく感じ。
つまりparentがnullになるまでずっとこのレーン更新処理をしていく。

Yug (やぐ)Yug (やぐ)

最後

if (isHidden && update !== null && node.tag === HostRoot) {
  const root: FiberRoot = node.stateNode;
  markHiddenUpdate(root, update, lane);
}

isHiddenがtrueになるのはさっき言った通りOffscreenの時だけなのでこれは飛ばして良さそうだな

Yug (やぐ)Yug (やぐ)

ということで、markUpdateLaneFromFiberToRoot関数は「末尾fiberからルートまで上がってレーンを更新していくだけの関数」と言えそうだ

Yug (やぐ)Yug (やぐ)

なので同時にfinishQueueingConcurrentUpdates関数もこれで終了

ReactFiberConcurrentUpdates.js
export function finishQueueingConcurrentUpdates(): void {
  const endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;

  concurrentlyUpdatedLanes = NoLanes;

  let i = 0;
  while (i < endIndex) {
    const fiber: Fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const queue: ConcurrentQueue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const update: ConcurrentUpdate = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const lane: Lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    if (queue !== null && update !== null) {
      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;
    }

    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}

この関数をまとめると...

  • concurrentQueues内をループでどんどん見ていって...
    • concurrentQueues内を全部nullで初期化していく
    • updateの循環リストを構築していく
      • その際にlaneがNoLaneではなければmarkUpdateLaneFromFiberToRoot実行でルートまで続くレーン更新作業もする
Yug (やぐ)Yug (やぐ)

そして更にその親であるenqueueConcurrentHookUpdateAndEagerlyBailoutも読み終わったことになる

ReactFiberConcurrentUpdates.js
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();
  }
}

やってることは2つ

  1. enqueueUpdate実行
  2. finishQueueingConcurrentUpdates関数実行(並行レンダー中ではない場合。FiberRootNodeがnullだったら並行レンダー中ではないという判断)
    • concurrentQueues内をループでどんどん見ていって...
      • concurrentQueues内を全部nullに初期化
      • updateの循環リストを構築していく
        • その際にlaneがNoLaneではなければmarkUpdateLaneFromFiberToRoot実行でルートまで続くレーン更新作業もする
Yug (やぐ)Yug (やぐ)

逆に言うと並行レンダー中の場合はキューイングをfinishしないってことのはずだが、
...それってどゆこと?並行レンダーの方を一旦優先したいからfinishしないでおく、的な?

あと、「並行レンダー中に更新が走り、かつ値に変更が見られなかった」っていうのが現状だと思うが、それってどういう状態?そもそも並行レンダーの中身と発生タイミングがわかってないのでイメージできてない

Yug (やぐ)Yug (やぐ)

変数名のenqueueConcurrentHookUpdateの部分はごちゃ混ぜ四兄弟を突っ込むことだろう

AndEagerlyBailoutの部分に該当するのはfinish(略)関数、つまりupdateの循環リスト構築やconcurrentQueues内をnullに初期化することかな?

でもレンダースキップ処理みたいなのが見当たらないのでbailoutしてるとは言えない気が...

いや、他の箇所ではこの後root.renderに繋がる処理をしているとか?それがbailout内だと書かれてないからレンダーされないよね、だから実質レンダーをスキップできるよね、みたいなこと?

わからんがとりあえず進めていくか

Yug (やぐ)Yug (やぐ)

これに戻ってくる

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

最後の部分

const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
  scheduleUpdateOnFiber(root, fiber, lane);
  entangleTransitionUpdate(root, queue, lane);
  return true;
}
Yug (やぐ)Yug (やぐ)

enqueueConcurrentHookUpdateはこれ

ReactFiberConcurrentUpdates.js
export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

enqueueUpdateはさっきやった。concurrentQueuesにごちゃ混ぜ四兄弟を突っ込む & レーンのmergeをする関数。

最後にgetRootForUpdatedFiberの結果を返してるが、これは何だ?

Yug (やぐ)Yug (やぐ)

これ

ReactFiberConcurrentUpdates.js
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
  // TODO: We will detect and infinite update loop and throw even if this fiber
  // has already unmounted. This isn't really necessary but it happens to be the
  // current behavior we've used for several release cycles. Consider not
  // performing this check if the updated fiber already unmounted, since it's
  // not possible for that to cause an infinite update loop.
  throwIfInfiniteUpdateLoopDetected();

  // When a setState happens, we must ensure the root is scheduled. Because
  // update queues do not have a backpointer to the root, the only way to do
  // this currently is to walk up the return path. This used to not be a big
  // deal because we would have to walk up the return path to set
  // the `childLanes`, anyway, but now those two traversals happen at
  // different times.
  // TODO: Consider adding a `root` backpointer on the update queue.
  detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
  let node = sourceFiber;
  let parent = node.return;
  while (parent !== null) {
    detectUpdateOnUnmountedFiber(sourceFiber, node);
    node = parent;
    parent = node.return;
  }
  return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}

さっき見たgetWorkInProgressRootと返り値は同じFiberRootNodeっぽいが、中身が違う

Yug (やぐ)Yug (やぐ)

TODO: 無限更新ループを検出し、このファイバーがすでにアンマウントされていてもスローします。これは本当に必要なことではありませんが、私たちがいくつかのリリースサイクルで使ってきた現在の動作です。更新されたファイバーがすでにアンマウントされている場合は、無限更新ループを引き起こす可能性がないため、このチェックを実行しないことを検討してください。

throwIfInfiniteUpdateLoopDetected();

ほー、無限ループに関連するところは凄い興味あるなぁ

throwIfInfiniteUpdateLoopDetectedはこれ

packages\react-reconciler\src\ReactFiberWorkLoop.js
export function throwIfInfiniteUpdateLoopDetected() {
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    nestedUpdateCount = 0;
    nestedPassiveUpdateCount = 0;
    rootWithNestedUpdates = null;
    rootWithPassiveNestedUpdates = null;

    if (enableInfiniteRenderLoopDetection) {
      if (executionContext & RenderContext && workInProgressRoot !== null) {
        // We're in the render phase. Disable the concurrent error recovery
        // mechanism to ensure that the error we're about to throw gets handled.
        // We need it to trigger the nearest error boundary so that the infinite
        // update loop is broken.
        workInProgressRoot.errorRecoveryDisabledLanes = mergeLanes(
          workInProgressRoot.errorRecoveryDisabledLanes,
          workInProgressRootRenderLanes,
        );
      }
    }

    throw new Error(
      'Maximum update depth exceeded. This can happen when a component ' +
        'repeatedly calls setState inside componentWillUpdate or ' +
        'componentDidUpdate. React limits the number of nested updates to ' +
        'prevent infinite loops.',
    );
  }

  if (__DEV__) {
    if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
      nestedPassiveUpdateCount = 0;
      rootWithPassiveNestedUpdates = null;

      console.error(
        'Maximum update depth exceeded. This can happen when a component ' +
          "calls setState inside useEffect, but useEffect either doesn't " +
          'have a dependency array, or one of the dependencies changes on ' +
          'every render.',
      );
    }
  }
}
Yug (やぐ)Yug (やぐ)

おー、50回までしか無限ループしないソースはこれか。へぇ

// Use these to prevent an infinite loop of nested updates
const NESTED_UPDATE_LIMIT = 50;
let nestedUpdateCount: number = 0;

ちなみにnestedUpdateCountがプラスされるのはcommitRootImplっていう関数内だった。重要そうだけどめちゃめちゃ長いので今回は無視

Yug (やぐ)Yug (やぐ)

戻ると、とりあえずthrowIfInfiniteUpdateLoopDetectedは「50回以上ループしてたらエラー出す」という関数

Yug (やぐ)Yug (やぐ)

んで更にgetRootForUpdatedFiberに戻る

ReactFiberConcurrentUpdates.js
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
  // TODO: We will detect and infinite update loop and throw even if this fiber
  // has already unmounted. This isn't really necessary but it happens to be the
  // current behavior we've used for several release cycles. Consider not
  // performing this check if the updated fiber already unmounted, since it's
  // not possible for that to cause an infinite update loop.
  throwIfInfiniteUpdateLoopDetected();

  // When a setState happens, we must ensure the root is scheduled. Because
  // update queues do not have a backpointer to the root, the only way to do
  // this currently is to walk up the return path. This used to not be a big
  // deal because we would have to walk up the return path to set
  // the `childLanes`, anyway, but now those two traversals happen at
  // different times.
  // TODO: Consider adding a `root` backpointer on the update queue.
  detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
  let node = sourceFiber;
  let parent = node.return;
  while (parent !== null) {
    detectUpdateOnUnmountedFiber(sourceFiber, node);
    node = parent;
    parent = node.return;
  }
  return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}

throwIfInfiniteUpdateLoopDetected呼び出しの後から

Yug (やぐ)Yug (やぐ)

setStateが発生したら、ルートがスケジュールされていることを確認しなければならない。updateキューはルートへのバックポインタを持たないので、現在これを行う唯一の方法は、returnパスを登っていくことである。以前は、childLanesを設定するためにリターンパスを登っていく必要があったので、これは大きな問題ではなかった。だが今はこの2つの横断は異なるタイミングで起こる。
TODO: updateキューに root バックポインタを追加することを検討する。

detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);

「この2つの横断」というのは恐らく「childLanesの設定」と「returnパスを登ること」かな

  • setStateが発生したら、ルートがスケジュールされていることを確認しなければならない
  • そのために今からreturnを登っていくよ

みたいな感じかな?

Yug (やぐ)Yug (やぐ)

detectUpdateOnUnmountedFiberはこれ

ReactFiberConcurrentUpdates.js
function detectUpdateOnUnmountedFiber(sourceFiber: Fiber, parent: Fiber) {
  if (__DEV__) {
    const alternate = parent.alternate;
    if (
      alternate === null &&
      (parent.flags & (Placement | Hydrating)) !== NoFlags
    ) {
      warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
    }
  }
}

あんま大事じゃなさそう

とりあえず、開発環境かつparent.alternateがnull、みたいな状況のときに「まだmountされてないのにupdateすんな!useEffect使え!」的なエラーログを出したりするっぽい

Yug (やぐ)Yug (やぐ)

戻る

function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
  // TODO: We will detect and infinite update loop and throw even if this fiber
  // has already unmounted. This isn't really necessary but it happens to be the
  // current behavior we've used for several release cycles. Consider not
  // performing this check if the updated fiber already unmounted, since it's
  // not possible for that to cause an infinite update loop.
  throwIfInfiniteUpdateLoopDetected();

  // When a setState happens, we must ensure the root is scheduled. Because
  // update queues do not have a backpointer to the root, the only way to do
  // this currently is to walk up the return path. This used to not be a big
  // deal because we would have to walk up the return path to set
  // the `childLanes`, anyway, but now those two traversals happen at
  // different times.
  // TODO: Consider adding a `root` backpointer on the update queue.
  detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
  let node = sourceFiber;
  let parent = node.return;
  while (parent !== null) {
    detectUpdateOnUnmountedFiber(sourceFiber, node);
    node = parent;
    parent = node.return;
  }
  return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}
Yug (やぐ)Yug (やぐ)
while (parent !== null) {
  detectUpdateOnUnmountedFiber(sourceFiber, node);
  node = parent;
  parent = node.return;
}

parentがnullにならない限り、ループでエラーログの関数出し続けて、どんどん上に上がっていく感じ

parentがnullになるということはルートに到達する時ということ?

あ、わかったかも
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#fiberツリーはどんな構成をしているのか

この図からわかるように、HostRootまできたらreturnを持たなくなるつまりnullになるということかもしれない
(nullではなくundefinedではないのか?という疑問があるが...まぁいいや)

Yug (やぐ)Yug (やぐ)

最後

return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;

tagはこれ。fiberの種類で、30種類あるやつ
https://zenn.dev/link/comments/4c56cfae43c5a6

なのでさっき見たように、HostRootになるはずなので三項演算子はtrueになり、(node.stateNode: FiberRoot)が返されるはず

この時点のnodeはHostRootというファイバーノード。このファイバーのstateNodeをFiberRoot型にキャストした上でreturnする

なるほどな、確かにHostRootのstateNodeプロパティは、これまでで言うreturnみたいなもので、上に上がるやつ。んで入っているのは確かにFiberRootNode。

Yug (やぐ)Yug (やぐ)

ということでgetRootForUpdatedFiber関数はFiberRootNodeを返すという点でgetWorkInProgressRoot関数と似てるが、追加で以下をやっていると雑に捉えておけば良いだろう

  • 無限ループに対するエラーログ出力
  • mount前のupdateに対するエラーログ出力
Yug (やぐ)Yug (やぐ)

ということでenqueueConcurrentHookUpdateも終了。

ReactFiberConcurrentUpdates.js
export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

まとめると...

  • enqueueUpdate実行
    • concurrentQueuesにごちゃ混ぜ四兄弟を突っ込む & レーンのmergeをする
  • FiberRootNodeを返す
Yug (やぐ)Yug (やぐ)

dispatchSetStateInternalに戻る

ReactFiberHooks.js
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;
}
Yug (やぐ)Yug (やぐ)

このrootにはFiberRootNodeが入る

const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

んでrootがnullじゃなかったらif文通過していろいろやってからreturn trueして終了してる

まずscheduleUpdateOnFiberから見る

Yug (やぐ)Yug (やぐ)

これ。でかすぎ

ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
) {
  if (__DEV__) {
    if (isRunningInsertionEffect) {
      console.error('useInsertionEffect must not schedule updates.');
    }
  }

  if (__DEV__) {
    if (isFlushingPassiveEffects) {
      didScheduleUpdateDuringPassiveEffects = true;
    }
  }

  // Check if the work loop is currently suspended and waiting for data to
  // finish loading.
  if (
    // Suspended render phase
    (root === workInProgressRoot &&
      workInProgressSuspendedReason === SuspendedOnData) ||
    // Suspended commit phase
    root.cancelPendingCommit !== null
  ) {
    // The incoming update might unblock the current render. Interrupt the
    // current attempt and restart from the top.
    prepareFreshStack(root, NoLanes);
    markRootSuspended(
      root,
      workInProgressRootRenderLanes,
      workInProgressDeferredLane,
      workInProgressRootDidSkipSuspendedSiblings,
    );
  }

  // Mark that the root has a pending update.
  markRootUpdated(root, lane);

  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    // This update was dispatched during the render phase. This is a mistake
    // if the update originates from user space (with the exception of local
    // hook updates, which are handled differently and don't reach this
    // function), but there are some internal React features that use this as
    // an implementation detail, like selective hydration.
    warnAboutRenderPhaseUpdatesInDEV(fiber);

    // Track lanes that were updated during the render phase
    workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
      workInProgressRootRenderPhaseUpdatedLanes,
      lane,
    );
  } else {
    // This is a normal update, scheduled from outside the render phase. For
    // example, during an input event.
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        addFiberToLanesMap(root, fiber, lane);
      }
    }

    warnIfUpdatesNotWrappedWithActDEV(fiber);

    if (enableTransitionTracing) {
      const transition = ReactSharedInternals.T;
      if (transition !== null && transition.name != null) {
        if (transition.startTime === -1) {
          transition.startTime = now();
        }

        // $FlowFixMe[prop-missing]: The BatchConfigTransition and Transition types are incompatible but was previously untyped and thus uncaught
        // $FlowFixMe[incompatible-call]: "
        addTransitionToLanesMap(root, transition, lane);
      }
    }

    if (root === workInProgressRoot) {
      // Received an update to a tree that's in the middle of rendering. Mark
      // that there was an interleaved update work on this root.
      if ((executionContext & RenderContext) === NoContext) {
        workInProgressRootInterleavedUpdatedLanes = mergeLanes(
          workInProgressRootInterleavedUpdatedLanes,
          lane,
        );
      }
      if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
        // The root already suspended with a delay, which means this render
        // definitely won't finish. Since we have a new update, let's mark it as
        // suspended now, right before marking the incoming update. This has the
        // effect of interrupting the current render and switching to the update.
        // TODO: Make sure this doesn't override pings that happen while we've
        // already started rendering.
        markRootSuspended(
          root,
          workInProgressRootRenderLanes,
          workInProgressDeferredLane,
          workInProgressRootDidSkipSuspendedSiblings,
        );
      }
    }

    ensureRootIsScheduled(root);
    if (
      lane === SyncLane &&
      executionContext === NoContext &&
      !disableLegacyMode &&
      (fiber.mode & ConcurrentMode) === NoMode
    ) {
      if (__DEV__ && ReactSharedInternals.isBatchingLegacy) {
        // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
      } else {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        resetRenderTimer();
        flushSyncWorkOnLegacyRootsOnly();
      }
    }
  }
}

これ見るのも良さそう
https://zenn.dev/calloc134/scraps/90649a1e7f02d9

Yug (やぐ)Yug (やぐ)

とりあえずGPTのまとめだけ貼っておいて、次に進んじゃうとする

Fiberの更新をスケジュールする、あといろいろやる、て感じで。。

Yug (やぐ)Yug (やぐ)

次はentangleTransitionUpdateを見るか

if (root !== null) {
  scheduleUpdateOnFiber(root, fiber, lane);
  entangleTransitionUpdate(root, queue, lane);
  return true;
}
Yug (やぐ)Yug (やぐ)

これ

// TODO: Move to ReactFiberConcurrentUpdates?
function entangleTransitionUpdate<S, A>(
  root: FiberRoot,
  queue: UpdateQueue<S, A>,
  lane: Lane,
): void {
  if (isTransitionLane(lane)) {
    let queueLanes = queue.lanes;

    // If any entangled lanes are no longer pending on the root, then they
    // must have finished. We can remove them from the shared queue, which
    // represents a superset of the actually pending lanes. In some cases we
    // may entangle more than we need to, but that's OK. In fact it's worse if
    // we *don't* entangle when we should.
    queueLanes = intersectLanes(queueLanes, root.pendingLanes);

    // Entangle the new transition lane with the other transition lanes.
    const newQueueLanes = mergeLanes(queueLanes, lane);
    queue.lanes = newQueueLanes;
    // Even if queue.lanes already include lane, we don't know for certain if
    // the lane finished since the last time we entangled it. So we need to
    // entangle it again, just to be sure.
    markRootEntangled(root, newQueueLanes);
  }
}

もしtransitionレーンだったら、レーンmergeしたりいろいろやる、てだけかな

Yug (やぐ)Yug (やぐ)

よし、これでやっと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;
}

まとめたい

Yug (やぐ)Yug (やぐ)

レンダーフェーズの更新だったら

  • enqueueRenderPhaseUpdateしたあとreturn falseして終了

レンダーフェーズの更新ではなかったら

  • alternate, lane, lastRenderedReducerとかを更新する、など
  • Object.isで値に変更が見られなかったら...
    • enqueueConcurrentHookUpdateAndEagerlyBailout実行。そしてreturn falseで終了
  • もしFiberRootNodeが存在したら...
    • scheduleUpdateOnFiberでfiberの更新をスケジュール
    • transitionレーンだったら、レーンをmergeしたりする
    • return trueで終了
  • どのreturnも通過せず最後まできたら、return falseで終了
Yug (やぐ)Yug (やぐ)

「変更が見られずレンダーが不要なのでbailoutする」というときはfalseだが、そうでない時は基本true、ていう感じっぽいので...

「レンダーをしたかどうか」?

いや、でも実際にレンダーを実行するコードはまったく無かったので、

レンダーをすべきかどうか
具体的には、
レンダーをスケジュールしたかどうか

っていう感じだと予想

Yug (やぐ)Yug (やぐ)

ということでやっとdispatchSetStateに戻ってきた

ReactFiberHooks.js
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);
}
Yug (やぐ)Yug (やぐ)

requestUpdateLaneは既にみたが、やってることとしては「fiberを受け取って、適切なレーンを返す」だった

それをlaneという変数で受け取る

Yug (やぐ)Yug (やぐ)

んでdispatchSetStateInternalを実行した結果返ってくる「レンダーすべきかどうか=レンダーをスケジュールしたかどうか」という情報をdidScheduleUpdateという変数で受け取る

あ、てかこの変数名を見る限り、「レンダーをスケジュールしたかどうか」というよりも「update(更新そそのもの)をスケジュールしたかどうか」と捉えた方が良いかもしれんなぁ
dispatchSetStateInternalの返り値

Yug (やぐ)Yug (やぐ)

んでそれがtrueだったらつまりupdateがスケジュールされていたら、startUpdateTimerByLane(lane);を実行。

startUpdateTimerByLaneを見に行くと、これ

packages\react-reconciler\src\ReactProfilerTimer.js
export function startUpdateTimerByLane(lane: Lane): void {
  if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
    return;
  }
  if (isSyncLane(lane) || isBlockingLane(lane)) {
    if (blockingUpdateTime < 0) {
      blockingUpdateTime = now();
      blockingEventTime = resolveEventTimeStamp();
      blockingEventType = resolveEventType();
    }
  } else if (isTransitionLane(lane)) {
    if (transitionUpdateTime < 0) {
      transitionUpdateTime = now();
      if (transitionStartTime < 0) {
        transitionEventTime = resolveEventTimeStamp();
        transitionEventType = resolveEventType();
      }
    }
  }
}
Yug (やぐ)Yug (やぐ)

resolveなんちゃらの関数はこれ

packages\react-dom-bindings\src\client\ReactFiberConfigDOM.js
export function resolveEventType(): null | string {
  const event = window.event;
  return event ? event.type : null;
}

export function resolveEventTimeStamp(): number {
  const event = window.event;
  return event ? event.timeStamp : -1.1;
}

windows.event??DOMの話になってくるのか

window.eventを取得するのは同じで、event.typeを返すかevent.timeStampを返すかの違い。

window.eventはイベントハンドラが引数で受け取るやつとは違うのか?
https://developer.mozilla.org/ja/docs/Web/API/Window/event
https://www.tohoho-web.com/js/event.htm

あー、window.eventではなく単にEventというインタフェースがあるが、こっちがモダンでありイベントハンドラが受け取るやつっぽい
https://developer.mozilla.org/ja/docs/Web/API/Event

event.typeはこれ。clickとかが入る
https://developer.mozilla.org/ja/docs/Web/API/Event/type

event.timeStampはこれ。イベントが作成された時刻が入る
https://developer.mozilla.org/ja/docs/Web/API/Event/timeStamp

window.eventもそれと同じようなもんだと考えていく(とりあえず)

Yug (やぐ)Yug (やぐ)

isBlockingLaneってやつも見てみたが、この4つのレーンに見覚えがあるな...

export function isBlockingLane(lane: Lane): boolean {
  const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane;
  return (lane & SyncDefaultLanes) !== NoLanes;
}

これだ。レンダーフェーズを中断できないレーン四兄弟だ
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#中断できないレーン

四兄弟のうちのどれかを判定する関数ってことか?

Yug (やぐ)Yug (やぐ)

ということでstartUpdateTimerByLaneをまとめよう

  • blockingレーンとtransitionレーンのみ扱う
  • updateTimeが負の数なら...
    • updateTimeをnow()で初期化
    • eventTimeとeventTypeをwindow.eventから取得(更新)
      • イベントが発生した時点の時間とtype
        • それとイベント処理完了時の時間の差分を見てパフォーマンス改善に繋げる...みたいなプロファイリングのためか?
Yug (やぐ)Yug (やぐ)

てことは...この関数名、とてもややこしくないか?

startUpdateTimerと言ってるが、これはUpdateTimerというTimerを開始している訳ではなくて、
TimerをUpdateするという行為をstartしているつまり実質的には開始ではなく更新しているだけにすぎない

なのでstart消してupdateTimerだけでいいのでは?という気はする

これ要注意だな(まぁ中身見ればわかることだけど)

とはいえ、理解はできた

Yug (やぐ)Yug (やぐ)

ということで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);
}
Yug (やぐ)Yug (やぐ)

最後のmarkUpdateInDevToolsはこれ

ReactFiberHooks.js
function markUpdateInDevTools<A>(fiber: Fiber, lane: Lane, action: A): void {
  if (__DEV__) {
    if (enableDebugTracing) {
      if (fiber.mode & DebugTracingMode) {
        const name = getComponentNameFromFiber(fiber) || 'Unknown';
        logStateUpdateScheduled(name, lane, action);
      }
    }
  }

  if (enableSchedulingProfiler) {
    markStateUpdateScheduled(fiber, lane);
  }
}

うーん、軽く見たけど全然わからなかった。
デバッグログ出したりスケジュールされたことを記録したりしてる?

開発環境限定だし、あんま大事じゃなさそうなので飛ばす

Yug (やぐ)Yug (やぐ)

ということでdispatchSetStateが終わったのでまとめたい

ReactFiberHooks.js
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);
}
  • setStateの第二引数が呼ばれててそれが関数だったら、ありえないのでエラー出す
  • requestUpdateLane実行
    • fiberに対して適切なレーンを返す
  • dispatchSetStateInternal実行
    • レンダーフェーズのupdateだった場合の例外処理やObject.isがtrueだった場合の例外処理をしたり、
    • concurrentQueues更新やscheduleUpdateOnFiberというバカ長い関数でfiber上のupdateをスケジュール?したりする
    • とりあえず、最終的にはupdateをスケジュールしたかどうかを返す
  • もしスケジュールしたら...
    • startUpdateTimerByLaneでタイマー更新
  • markUpdateInDevTools実行
    • ようわからんがデバッグログ出したりする
Yug (やぐ)Yug (やぐ)

超大事なのはdispatchSetStateInternalで、startUpdateTimerByLaneによるタイマー更新も大事

て感じでまとめて良い気がする

とにかくdispatchSetStateInternalがデカくて大事

んでそのdispatchSetStateInternalの中で超大事なのは多分scheduleUpdateOnFiberだなー

まぁ要は

例外処理を除けば、dispatchSetStateInternalは「fiber上のupdateをスケジューリングする関数」とまとめて良さそうで、ほぼそれを使ってるだけなのがdispatchSetState

的なイメージ

Yug (やぐ)Yug (やぐ)

ということで百年ぶりにmountStateに戻ってくる。まとめたい

ReactFiberHooks.js
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];
}
  • mountStateImpl実行
    • mountWorkInProgressHook実行でhook初期化
    • 初期化関数を2回実行、それに伴いstrictMode更新
    • hook.memoizedStateを受け取った初期値で更新
    • それを反映した上でのhook.queue更新
    • 最後にhookを返す
  • dispatchSetStateをbindしたものをdispatchとして受け取る
  • 最後に[hook.memoizedState, dispatch]をreturnして終了
Yug (やぐ)Yug (やぐ)

なのでupdateをスケジューリングしておくということもしているとはいえ、すぐにhook.memoizedStateは更新されて、すぐにreturnされるというのがmount時の挙動っぽい

...ならupdateのスケジューリングいらなくね?

Yug (やぐ)Yug (やぐ)

dispatchSetStateをbindするというところに関して。

dispatchSetStateの引数はこの3つ

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
)

だがbindの際に第一引数で渡したもの(null)はthisの参照先になる。なので別の話。
->渡したのは実質fiber(実質第一引数)とqueue(実質第二引数)のみということになる。

なのでdispatchがユーザに使われるとき第一引数が設定されたら、それが初めてdispatchSetStateでの実質第三引数となり、それこそがaction: Aの部分になる。

なるほど~

Yug (やぐ)Yug (やぐ)

とりあえず終わった~~~~~~

残った疑問点などはこれ

  • memoizedStateはすぐに更新してるからupdateをスケジューリングする必要なくね?
  • scheduleUpdateOnFiberというバカ長関数をまだ見れてない
  • スケジューリングどこで拾われるの?
    • つまりレンダーはどこでどうやって起きるの?
  • setStateによってupdateのスケジュールとかはされてたけど、レンダーのスケジュールはされてないの?されてるはずなんだが
  • 並行レンダーって普通のレンダーと何が違うの?
    • どういうものが並行レンダーとして選ばれるの?基準は?
    • useTransitionの内部実装とほぼイコールか?