Kodokon kodokon.com

内部の仕組み: 再調整、バッチ処理、並行レンダリング

Fiber がどのようにツリーを再調整し、更新をバッチ処理し、レンダリング作業を中断可能にするかを学びます。

11 分 · 3 問

このレッスンを Kodokon で開く

React 16 以降、内部エンジンは Fiber と呼ばれています。ツリー内のすべての要素は、作業単位である fiber ノードとして表現されます。React は実際には 2 つのツリー(ダブルバッファリングという手法)を保持しています。画面に表示されている内容を反映する current ツリーと、レンダリング中に構築される workInProgress ツリーです。作業は 2 つの明確に異なるフェーズで行われます。レンダーフェーズ(コンポーネントを呼び出し、差分を計算する)は中断可能で再実行可能であり、次に コミットフェーズ(DOM への変更を適用し、エフェクトを実行する)は常に 同期的かつ中断不可能 です。再調整は、比較を線形コストに抑える 2 つのヒューリスティックに依存しています。ある位置で要素の が変わると、React はサブツリー全体を破棄し、state も含めて再構築します。リストの場合、key プロパティによって React は 2 回のレンダー間で要素を対応付けられます。

JSX
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <input defaultValue={item.label} />
        </li>
      ))}
    </ul>
  );
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.
安定した key は各行の state と DOM を保持します。

2 つ目の基本的なメカニズム: バッチ処理。同じコンテキスト内で複数の state 更新がトリガーされると、React はそれらをまとめて 単一のレンダー にします。React 18 より前は、このグループ化は React のイベントハンドラー内でしか機能しませんでした。createRoot 以降は、タイマー、Promise、ネイティブリスナーなど あらゆる場所で自動的 に行われます。これはまた、setState の直後に state を読み取ると古い値が返る理由でもあります。更新はスケジュールされるのであって、即座に適用されるわけではないのです。

JSX
function handleTick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // React 18: a single render, even here.
}

setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.
自動バッチ処理はあらゆるコンテキストをカバーするようになりました。

並行レンダリング はこのロジックをさらに推し進めます。より緊急な更新が入ってきた場合、レンダーフェーズは 中断、再開、あるいは破棄して再開 することができます。React は更新を優先度(lanes)で順位付けします。キー入力は緊急ですが、startTransition でマークされたリストフィルターは緊急ではありません。その直接的な結果として、あなたのコンポーネント関数は 純粋 でなければなりません。React は結果をコミットすることなく、それらを何度も呼び出す可能性があるからです。もう 1 つの特殊なケース: 中断可能なレンダー中に読み取られる外部ストア(Redux、Zustand、自作のシングルトン)は レンダーの途中で 変化する可能性があり、tearing(テアリング) として知られる一貫性のないインターフェースを生み出します。useSyncExternalStore API は、ストアが変化したときに同期レンダーを強制することで、この問題を解決します。

JAVASCRIPT
import { useSyncExternalStore } from 'react';

function useWindowWidth() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('resize', notify);
      return () =>
        window.removeEventListener('resize', notify);
    },
    () => window.innerWidth
  );
}
tearing のリスクなしに外部ストアを読み取る。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. 再調整中、同じ位置にある <div> 要素が <span> に変わると React は何をしますか?
    • 子要素を保持しながらタグを更新する
    • state も含めてサブツリー全体を破棄し、再構築する
    • 決定する前に子要素を再帰的に比較する
    • 再調整エラーをスローする
  2. React 18 と createRoot を使う場合、setTimeout 内の 2 回の setState 呼び出しは何回のレンダーをトリガーしますか?
    • 2 回。バッチ処理は React のイベントにしか適用されないため
    • 1 回のみ。自動バッチ処理のおかげ
    • 0 回。イベント外の更新は無視されるため
  3. 並行モードにおいて、React の作業のどのフェーズが中断可能ですか?
    • コミットフェーズのみ
    • レンダーフェーズのみ
    • 両方のフェーズ
    • どちらでもない。React は完全に同期的なまま