5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useState の内部構造をソースコードから理解する【共通処理編】

5
Posted at

本記事は、React の内部実装を理解するための学習ログです。
前回の概要編に続き、今回は mountState / updateState に入る前の共通処理を読んでいきます。

はじめに

概要編では、useState の大まかな仕組みを整理しました。

useState が動くまでには、実は useState が呼ばれる前に React 側の準備処理があります。
実際の処理の流れを大きく示すと次のようになります。

【React 内部】

renderWithHooks() が呼ばれる   ← React がコンポーネントを実行する前の準備
  ↓
Dispatcher をセット             ← 今回はここ
  ↓
コンポーネント関数を実行
  ↓
【コンポーネント内】

useState(10) が呼ばれる
  ↓
① Dispatcher の解決            ← 今回はここ
  ↓
② mount か update かの振り分け  ← 今回はここ
  ↓
③ mountState / updateState の実行
  ↓
④ Hook ノードの生成・Fiber への紐付け
  ↓
⑤ queue の初期化
  ↓
⑥ [state, dispatch] を返す

【React 内部】

finishRenderingHooks()         ← Dispatcher をリセット

今回の記事では、renderWithHooks による Dispatcher のセット(準備処理)と、useState 内部の①②を読んでいきます。
具体的には「同じ useState がなぜ初回と更新で別の動きをするのか」という仕組みの部分です。

③以降は次回以降の記事で扱います。

useState を呼ぶと最初に何が起きるか

まず、useState の実装を見てみましょう。

// packages/react/src/ReactHooks.js
export function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

驚くほど薄いです。

useState 自体はほとんど何もしておらず、Dispatcher というオブジェクトに丸投げしています。

ポイント:useState は薄いラッパーに過ぎない。実体は Dispatcher 側にある。

Dispatcher とは何か

resolveDispatcher() の実装を見てみます。

// packages/react/src/ReactHooks.js
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        "Invalid hook call. Hooks can only be called inside of the body of a function component...",
      );
    }
  }
  return dispatcher;
}

ReactSharedInternals.H を返しているだけです。

ReactSharedInternals とは何か?

import している箇所を見ると、shared/ReactSharedInternals からインポートしています。
中身を確認すると、

import * as React from "react";

const ReactSharedInternals =
  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;

export default ReactSharedInternals;

React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE は、React が内部処理のために使うグローバルな共有オブジェクトです。
名前が非常に長いのは意図的で、「外部から触るな」というメッセージをそのまま名前にしています。
このオブジェクトを ReactSharedInternals という短い名前で export して、React 内部の様々なファイルから参照しやすくしています。


この H「今どのモードで Hook を実行すべきか」を表す Dispatcher です。

Dispatcher はオブジェクトで、useStateuseEffect などの Hook の実装が入っています。

Dispatcher(イメージ)
  useState: mountState  または  updateState
  useEffect: mountEffect  または  updateEffect
  ...

重要なのは、同じ useState でも初回と2回目以降では別の関数が呼ばれるという点です。

【初回レンダー】                    【再レンダー】

useState(10)                       useState(10)
    ↓                                  ↓
resolveDispatcher()                resolveDispatcher()
    ↓                                  ↓
H = HooksDispatcherOnMount         H = HooksDispatcherOnUpdate
    ↓                                  ↓
dispatcher.useState                dispatcher.useState
= mountState(10)                   = updateState(10)

Dispatcher はいつ・どうセットされるか

ReactSharedInternals.HrenderWithHooks の中でセットされます。

renderWithHooks はコンポーネントを実行する前に呼ばれる関数で、
ここで「このコンポーネントは初回か更新か」を判断し、適切な Dispatcher をセットします。

// packages/react-reconciler/src/ReactFiberHooks.js(一部抜粋)
export function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderLanes,
) {
  // ...

  ReactSharedInternals.H =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount // 初回
      : HooksDispatcherOnUpdate; // 更新

  // コンポーネント関数を実行(ここで useState が呼ばれる)
  let children = Component(props, secondArg);

  // ...

  finishRenderingHooks(current, workInProgress, Component);

  return children;
}

判定条件は current === null || current.memoizedState === null です。

次のセクションでこの条件の意味を見ていきます。

current.memoizedState とは何か

current は「前回レンダー済みの Fiber」です。

その memoizedState は、その Fiber に紐づく Hook リンクリストの先頭ポインタです。

Fiber (current)
 └─ memoizedState
        ↓
      Hook1 (useState)
        ↓ next
      Hook2 (useState)
        ↓ next
      Hook3 (useEffect)

つまり判定条件の意味はこうです。

current === null
  → Fiberがそもそも存在しない(初めてマウントされるコンポーネント)
  → mount

current.memoizedState === null
  → Fiberは存在するが、Hookをひとつも持っていない
  → mount

上記以外
  → 前回レンダー済みのHookが存在する
  → update

レンダーが終わったら Dispatcher はリセットされる

コンポーネントの実行が終わると、finishRenderingHooks が呼ばれます。

function finishRenderingHooks(current, workInProgress, Component) {
  // ...
  ReactSharedInternals.H = ContextOnlyDispatcher;
  // ...
}

HContextOnlyDispatcher に上書きされます。

この Dispatcher は、Hook を呼ぶと必ずエラーを投げる実装になっています。

ContextOnlyDispatcher
  useState: throwInvalidHookError  // ← 呼ぶとエラー
  useEffect: throwInvalidHookError
  ...

これが「コンポーネントの外で useState を呼ぶと Invalid hook call になる」理由です。

【レンダー中】
H = HooksDispatcherOnMount / HooksDispatcherOnUpdate
  → useState が正常に動く

【レンダー外】
H = ContextOnlyDispatcher
  → useState を呼ぶとエラー

ポイント:Dispatcher は render フェーズ中だけ有効で、終わったら即リセットされる。

まとめ:共通処理の全体像

今回読んだ処理の流れをまとめます。

【React 内部】

renderWithHooks() が呼ばれる
  ↓
current の状態を確認して Dispatcher をセット
  current === null || current.memoizedState === null
    → HooksDispatcherOnMount  (初回)
    → HooksDispatcherOnUpdate (更新)
  ↓
コンポーネント関数を実行

【コンポーネント内】

useState(10) が呼ばれる
  ↓
① Dispatcher の解決
   resolveDispatcher() → ReactSharedInternals.H を取得
  ↓
② mount か update かの振り分け
   dispatcher.useState(10) を呼ぶ
     → mountState(10)  (初回)
     → updateState(10) (更新)
  ↓
③ mountState / updateState の実行 ← 次回以降
  ↓
④ Hook ノードの生成・Fiber への紐付け ← 次回以降
  ↓
⑤ queue の初期化 ← 次回以降
  ↓
⑥ [state, dispatch] を返す ← 次回以降

【React 内部】

finishRenderingHooks()
  ↓
H = ContextOnlyDispatcher(リセット)

次の記事では

次回は mountState の中身を読んでいきます。

初回レンダーで useState(10) が呼ばれたとき、

  • Hook ノードがどう生成されるか
  • Fiber にどう紐づけられるか
  • リンクリストがどう構築されるか

mountWorkInProgressHook() のソースコードを追いながら見ていきます。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?