banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useState の分析

説明#

  1. 本文は v18.1.0 に基づいて分析されています。
  2. useState のデバッグを行う際に、interleaved という概念が繰り返し登場しますので、こちらの リンク を参照してください。
  3. 記事はおおむね以下のコードに基づいて分析されており、状況に応じて若干の変更があります。
  4. 2022-10-14 にバッチ処理に関するソースコード分析(タスク再利用【関連】および 高優先度の割り込み【コード内で軽く触れています】)とスケジューリング段階の簡単なまとめが追加されました。
import { useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => { setText(1) } }>{ text }</div>
  )
}

export default UseState;

TLNR#

  1. useState の dispatch は非同期操作です。
  2. useState の実装は useReducer に基づいており、言い換えれば useState は特別な useReducer です。
  3. useState を呼び出すたびに対応する update インスタンスが生成され、同じ dispatch を複数回呼び出すと、1 つの update の連結リストとしてマウントされます。また、各 update には対応する優先度(lane)が存在します。
  4. 生成された update がスケジュール更新が必要と判断されると、microTask の形式でスケジュール更新されます。
  5. useState の update 段階内では、高優先度の update に迅速に応答するために、update 連結リストは高優先度の update のみを先に処理し、低優先度の update は次回の render 段階で更新されます。
  6. バッチ処理と(スケジューリング → 調整 → レンダリング)内のスケジューリング段階のまとめについては文末をご覧ください。

mount シーンでの useState#

useState にブレークポイントを直接設定してデバッグすると、mountState 内に入ります。

useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  ···
    // initialState は useState に渡された初期値
    return mountState(initialState);
  ···
}

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // hooks 連結リスト生成手順については[以前の記事](https://github.com/IWSR/react-code-debug/issues/2)を参照
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // initialState の型宣言はそれが関数であることも示しています
    initialState = initialState();
  }
  // hook 上の memoizedState、baseState は useState の初期値をキャッシュします
  hook.memoizedState = hook.baseState = initialState;
  // 更新時に言及される、重要な構造
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue; // useState に対応する hooks オブジェクトは複数の queue 属性を持ちます
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any)); // 本文の分析の焦点、非常に重要な関数
  // const [text, setText] = useState(0); の返り値に対応
  return [hook.memoizedState, dispatch];
}

このようにして、mount 段階の useState の分析が完了しました。内容は非常に少ないです。

また、basicStateReducer という関数にも注意してください。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

まとめ#

mount 段階の useState は主に初期化の役割を果たし、この関数の呼び出しが終了すると、対応する hooks オブジェクトを取得します。このオブジェクトは次のようになります。

image

useState の dispatch をトリガーする#

この時点で、useState が返す dispatch にブレークポイントを設定し、クリックイベントをトリガーします。以下の呼び出しスタックを取得します。

image

dispatchSetState は重要な関数であることを再度強調します。

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

  /**
   * ここでは現在の update の優先度を取得することを理解してください
  */
  const lane = requestUpdateLane(fiber); 

  /**
   * setState の update 連結リストに似ています
   * ここでの update も連結リスト構造です
  */
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  /**
   * レンダリング中の更新かどうかを判断します
  */
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    /**
     * React が現在アイドル状態かどうかを判断します
     * 例えば、最初の dispatch 呼び出し後、fiber.lanes は NoLanes ではなくなります
     * したがって、2 回目の dispatch 呼び出しは下の eagerState の評価に入ることはありません
    */
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // キューは現在空であり、次の状態をレンダリングフェーズに入る前に先に計算できます。
      // 新しい状態が現在の状態と同じであれば、完全にバイアウトできるかもしれません。
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;

        const currentState: S = (queue.lastRenderedState: any);
        /**
         * 期待される状態を計算し、eagerState に保存します
         * lastRenderedReducer 内のロジックは以下の通りです
         * 現在の action が関数かどうかを判断します
         * もちろん、setText((preState) => preState + 1) のような呼び出し方法も存在します
         * もし関数であれば currentState を渡して action を呼び出し、計算された値を取得します
         * もしそうでなければ、例えば setText(1) のような呼び出し方法であれば、action をそのまま返します
        */
        const eagerState = lastRenderedReducer(currentState, action);
        // eagerState を計算し、計算に使用された reducer を update オブジェクトに保存します。
        // render フェーズに入るまでに reducer が変更されていなければ、eager state を再度呼び出すことなく使用できます。
        update.hasEagerState = true; // コメントに基づいて、reducer の再計算を防ぎます
        update.eagerState = eagerState;
        /**
         * 両者の値が等しいかどうかを浅く比較します ===
        */
        if (is(eagerState, currentState)) {
          // 高速パス。React に再レンダリングをスケジュールする必要がないため、バイアウトできます。
          // 異なる理由でコンポーネントが再レンダリングされる場合、update を再ベース化する必要があるかもしれません。
          // TODO: この場合、トランジションを絡める必要がありますか?
          /**
           * コメントに基づいて、値が変更されていない場合、React に再レンダリングをスケジュールする必要はありません。
           * 下の関数は update 連結リストの構造を処理するだけです。
          */
          enqueueConcurrentHookUpdateAndEagerlyBailout(
            fiber,
            queue,
            update,
            lane,
          );
          return; // スケジュールする必要がないため、関数はここで中断します。
        }
        
      }
    }
    /**
     * ここに到達した update は、ページに再レンダリングされる必要があると見なされます。
     * enqueueConcurrentHookUpdate と enqueueConcurrentHookUpdateAndEagerlyBailout は異なり、
     * markUpdateLaneFromFiberToRoot を呼び出します。
     * この関数は、現在の update に対応する lane を、更新が発生した fiber ノードから親 fiber の childLanes に向かって再帰的にマークします(fiber.return)。
     * ルートまで続きます。
     * markUpdateLaneFromFiberToRoot を呼び出す目的は、scheduleUpdateOnFiber のスケジューリングの準備をすることです。
    */
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      /**
       * スケジューリングプロセスに入ります(scheduleUpdateOnFiber の分析は最下部に更新されています)
       * ただし、v18 では queueMicrotask に登録され、microTask になります。
       * v17 では、対応する優先度でスケジューラに登録されます。
      */
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      /**
       * 不安定な関数であり、主に lanes を処理しますが、少し難解で解析しません。
      */
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane, action);
}

useState の dispatch をまとめる#

useState の dispatch に関しては、dispatch を呼び出すたびに新しい update インスタンスが生成され、そのインスタンスは対応する hooks の連結リストにマウントされますが、dispatch を呼び出した後に生成された新しい値は、元の値(currentState)と浅く比較され、この更新がスケジュール更新される必要があるかどうかを決定します。v18 のバージョンでは、このスケジュールタスクは microTask に存在するため、useState の dispatch は非同期操作であると考えることができます。以下に簡単な例を示して検証します。

import { useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => { setText(1); console.log(text); } }>{ text }</div>
  )
}

export default UseState;

クリック後に得られる結果

image

その呼び出しスタックは、microtask が見えることがわかります。

image

update シーンでの useState#

次に、元のデバッグコードに dispatch を追加します。新しいデバッグコードは次のようになります。

import { useEffect, useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => {
      setText(1);
      setText(2); // ここにブレークポイントを設定します
    } }>{ text }</div>
  )
}

export default UseState;

ページ全体の動作を記録すると、次のようになります。

image

ちなみに、2 回目の dispatch 時には fiber.lanes === NoLanes という条件を満たさないため、直接 enqueueConcurrentHookUpdate に入ります(具体的には dispatchSetState のコードを確認してください)。

enqueueConcurrentHookUpdate によって update 連結リストが接続された後、次のような環状連結リストの構造が得られます。

image

本題に戻りますが、useState の update 段階では updateState 内の updateReducer という関数が呼び出されます(これは useReducer に対応する mount バージョンです)。

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  /**
   * この関数は use(Layout)Effect 内でも登場し、主な役割は
   * WIP hooks と current hooks のポインタを同時に1つ後ろに移動させることです。
   * 詳細な注釈は [React Hooks: 附録](https://github.com/IWSR/react-code-debug/issues/4) を参照してください。
  */
  const hook = updateWorkInProgressHook(); // WIP hooks オブジェクト
  const queue = hook.queue; // update 連結リスト

  if (queue === null) {
    throw new Error(
      'キューが必要です。これはおそらく React のバグです。問題を報告してください。',
    );
  }

  // useState のシーンでは lastRenderedReducer は basicStateReducer です —— mountState で事前に設定された reducer
  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // 最後の再ベース化された update で、ベース状態の一部ではありません。
  let baseQueue = current.baseQueue;

  // まだ処理されていない最後の保留中の更新。
  // 保留中の状態の update を取得し、まだ計算されていません。
  const pendingQueue = queue.pending;
  /**
   * update queue を整理し、次の更新の準備をします。
  */
  if (pendingQueue !== null) {
    // 新しい更新がまだ処理されていない場合。
    // pendingQueue を baseQueue に追加します。
    // pendingQueue と baseQueue を接続します。
    if (baseQueue !== null) {
      // pendingQueue と baseQueue をマージします。
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  /**
   * 処理された baseQueue にはすべての update インスタンスが含まれます。
  */
  if (baseQueue !== null) {
    // 処理するキューがあります。
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = update.lane;
      /**
       * renderLanes 内に含まれる update をフィルタリングします。
       * このステップはビット操作に関係しています。
       * updateLane が renderLanes 内に属する場合、現在の update の優先度が比較的緊急であることを示します。
       * 処理が必要です。逆に、スキップできます。
       * 同様のロジックはクラスコンポーネントの update 連結リストにも存在します(ここでは詳細には展開しません)。
       * ただし、ここでは ! 操作があるため、すべて反対です。
      */
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 優先度が不十分です。スキップします。この最初のスキップされた update の場合、前の update/state が新しいベース
        // update/state になります。
        /**
         * このロジックに入った update は、あまり緊急ではなく、ゆっくり処理できます。
         * ただし、処理しないわけではなく、アイドル状態のときにスキップされた update から再計算を開始します。
         * したがって、スキップされた update インスタンスをキャッシュする必要があります。
        */
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        /**
         * スキップされた update を newBaseQueue に追加します。
         * 次回の render で再計算します。
        */
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // キュー内の残りの優先度を更新します。
        // TODO: これを蓄積する必要はありません。代わりに、元の lanes から renderLanes を削除できます。
        // WIP fiber の lanes を更新し、次回の render でスキップされた update を再実行します。
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        // スキップされた lanes を workInProgressRootSkippedLanes にマークします。
        markSkippedUpdateLanes(updateLane);
      } else {
        // この update は十分な優先度を持っています。
        // 優先度が十分な場合、更新を実行します。

        if (newBaseQueueLast !== null) {
          /**
           * newBaseQueueLast が null でない場合、スキップされた update が存在することを示します。
           * update の状態計算には関連性があるかもしれません。
           * したがって、update がスキップされると、スキップされた update を起点として、
           * その後のすべての update を優先度に関係なく切り取ります。
           * (クラスコンポーネントの処理ロジックに似ています)
          */
          const clone: Update<S, A> = {
            // この update はコミットされるため、未コミットにすることはありません。NoLane を使用することで、0 はすべてのビットマスクの部分集合であるため、
            // これは上記のチェックによってスキップされることはありません。
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // この update を処理します。
        // この update が eagerState を持っている場合、すでに計算された状態がマークされ、再計算を防ぎます。
        // この update が状態更新(reducer ではない)であり、先に処理された場合、
        // eagerState を使用できます。
        newState = ((update.eagerState: any): S);
        } else {
          // state と action に基づいて新しい state を計算します。
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // 新しい状態が現在の状態と異なる場合、fiber が作業を行ったことをマークします。
    // === チェックが通らない場合、現在の WIP fiber に更新がマークされ、completeWork 段階で root に収集されます。
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    // 新しいデータを hook に更新します。
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  // interleaved updates は別のキューに保存されます。これらはこのレンダリング中に処理されませんが、残りの lanes を追跡する必要があります。
  /**
   * interleaved update を処理します。
   * ただし、どのような更新が interleaved update と呼ばれるのか全く分からないため、分析をスキップします。
   * /react-reconciler/src/ReactFiberWorkLoop.old.js 内にこの部分の説明があります。
   * レンダリング中のツリーへの更新を受け取りました。これにより、このルートで interleaved update 作業があったことがマークされます。
   * ただし、enqueueConcurrentHookUpdate と 
   * enqueueConcurrentHookUpdateAndEagerlyBailout 内での queue.interleaved の設定とは完全に関連していません。
   * この分野に関する理解がある方は、ぜひ教えてください。
  */
  const lastInterleaved = queue.interleaved;
  if (lastInterleaved !== null) {
    let interleaved = lastInterleaved;
    do {
      const interleavedLane = interleaved.lane;
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        interleavedLane,
      );
      markSkippedUpdateLanes(interleavedLane);
      interleaved = ((interleaved: any).next: Update<S, A>);
    } while (interleaved !== lastInterleaved);
  } else if (baseQueue === null) {
    // `queue.lanes` はトランジションを絡めるために使用されます。キューが空になったら、再びゼロに戻すことができます。
    queue.lanes = NoLanes;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

まとめ#

useState の update 段階では、dispatch 関数によって生成された update 連結リストを使用して対応する状態を更新しますが、すべての update が更新されるわけではなく、優先度(lane)が現在の renderLanes 内に属する場合にのみ優先的に計算され、スキップされた update はマークされ、次回の render 時に更新されます。

簡単な例を提供して証明します。

import { useEffect, useState, useRef, useCallback, useTransition } from "react";

function UseState() {
  const dom = useRef(null);
  const [number, setNumber] = useState(0);
  const [, startTransition] = useTransition();

  useEffect(() => {
    const timeout1 = setTimeout(() => {
      startTransition(() => { // 優先度を下げ、timeout2 内の update に割り込まれる
        setNumber((preNumber) => preNumber + 1);
      });
    }, 500 )

    const timeout2 = setTimeout(() => {
      dom.current.click();
    }, 505)

    return () => {
      clearTimeout(timeout1);
      clearTimeout(timeout2);
    }
  }, []);

  const clickHandle = useCallback(() => {
    console.log('click');
    setNumber(preNumber => preNumber + 2);
  }, []);

  return (
    <div ref={dom} onClick={ clickHandle }>
      {
        Array.from(new Array(20000)).map((item, index) => <span key={index}>{ number }</span>)
      }
    </div>
  )
}

export default UseState;

実行結果から見ると、timeout1 の update が startTransition によって降格され、timeout2 の update が優先的に更新されました。

Sep-07-2022 16-57-09

バッチ処理#

バッチ処理は実際には v17 時代から存在していましたが、v18 ではその制限条件が取り除かれ、自動バッチ処理になりました(厄介です)。具体的にはこの 記事 を参照してください。

例えば、以下のコードが実行されるとします(もちろん、ここでのデバッグ環境は依然として v18 です)。

import { useEffect, useState } from "react";

function UseState() {
  const [num1, setNum1] = useState(1);
  const [num2, setNum2] = useState(2);

  useEffect(() => {
    setNum1(11);
    setNum2(22);
  }, []);

  return (
    <>
      <div>{ num1 }</div>
      <div>{ num2 }</div>
    </>
  )
}

上記の dispatchSetState の分析に基づくと、ページに更新が必要な update がある場合、scheduleUpdateOnFiber が呼び出され、レンダリングがトリガーされます。したがって、例の中で連続して 2 回 dispatch を呼び出した場合、ページは何回レンダリングされるのでしょうか?

image

1 回だけです。

なぜなら、scheduleUpdateOnFiber 内で何が行われているのかを確認する必要があります。scheduleUpdateOnFiber は全体の更新の入り口を処理するため、非常に重要な関数です。

この関数は主に以下を処理します。

  1. 無限更新が存在するかどうかを確認します —— checkForNestedUpdates
  2. root.pendingLanes に更新が必要な update の lanes をマークします —— markRootUpdated
  3. ensureRootIsScheduled をトリガーし、タスクスケジューリングのコア関数に入ります。

scheduleUpdateOnFiber#

  export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // ルートが同期的に再レンダリングされる回数をカウントします。
  // 完了せずに再レンダリングされる場合、無限更新ループを示します。
  checkForNestedUpdates();

  // ルートに保留中の更新があることをマークします。
  markRootUpdated(root, lane, eventTime);

  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    /**
     * エラーハンドリング、スキップ
    */
  } else {
    ...
    /**
     * 重要な関数!タスクスケジューリングのコア
     * 我々が探しているロジックもここにあります。
    */
    ensureRootIsScheduled(root, eventTime);
    ...
    // 以下はレガシーモードの互換コードであり、見ません。
  }
}

ここで、React 内の重要な関数である ensureRootIsScheduled に遭遇します。この関数に入ると、タスクスケジューリングにおけるコアロジックが見えてきます(Scheduler 内にも一部のコアロジックがあります。私が書いた React Scheduler: Scheduler ソースコード分析 を参照してください)。

ensureRootIsScheduled#

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  /**
   * root.callbackNode は Scheduler がスケジューリング時に生成するタスクであり、
   * この値は React が scheduleCallback を呼び出すときに返され、root.callbackNode に割り当てられます。
   * 既存のコールバックノードの変数名から推測できるように、これは既にスケジュールされたタスク(旧タスク)を示します。
  */
  const existingCallbackNode = root.callbackNode;

  // 他の作業によって飢餓状態にある lanes があるかどうかを確認します。もしあれば、それらを期限切れとしてマークします。
  markStarvedLanesAsExpired(root, currentTime);

  // 次に作業を行う lanes とその優先度を決定します。
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  /**
   * renderLanes が空であれば、現在スケジューリングを開始する必要がないことを意味し、スキップします。
  */
  if (nextLanes === NoLanes) {
    // 特殊なケース:作業するものがありません。
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // 最も優先度の高い lane を使用してコールバックの優先度を表します。
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // 既存のタスクがあるかどうかを確認します。再利用できるかもしれません。
  const existingCallbackPriority = root.callbackPriority;
  /**
   * もし両方のタスクの優先度が同じであれば、スキップします。既に対応するタスクが存在するため、再利用できます。
  */
  if (
    existingCallbackPriority === newCallbackPriority
  ) {
    // 優先度が変わっていないため、既存のタスクを再利用できます。終了します。
    return;
  }

  /**
   * 割り込みロジック
   * ここに入るロジックはすべて、旧タスクの優先度よりも高いと見なされるため、再スケジュールする必要があります。
   * したがって、旧タスクは呼び出す必要がなくなり、キャンセルできます(キャンセルロジックは Scheduler 内のロジックを参照)。
  */
  if (existingCallbackNode != null) {
    // 既存のコールバックをキャンセルします。以下で新しいものをスケジュールします。
    cancelCallback(existingCallbackNode);
  }

  // 新しいタスクをスケジュールします。
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // 特殊なケース:同期 React コールバックは特別な内部キューにスケジュールされます。
    /**
     * 同期優先度は、期限切れのタスクまたは現在のモードが非 concurrent モードであることを意味します。
    */
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    if (supportsMicrotasks) {
      // マイクロタスク内でキューをフラッシュします。
      /**
       * ブラウザがマイクロタスクをサポートしている場合、performSyncWorkOnRoot をマイクロタスク内に投げ込みます。
       * これにより、より迅速に実行できます。
       * 1 つの eventLoop は、古い MacroTask → マイクロタスクをクリア → ブラウザがページをレンダリングするという順序で実行されます。
       * さもなければ、Scheduler 内に入ると、MessageChannel で処理されるタスクはすべて MacroTask であり、次の
       * eventLoop 内で再レンダリングされます。
      */
      scheduleMicrotask(() => {
        if (
          (executionContext & (RenderContext | CommitContext)) ===
          NoContext
        ) {
          /**
           * ここでは、上記でスケジュールされたタスクをキャンセルし、performSyncWorkOnRoot を呼び出します。
          */
          flushSyncCallbacks();
        }
      });
    } else {
      // 即時タスク内でキューをフラッシュします。
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    // concurrent モードの処理ロジック
    let schedulerPriorityLevel;
    // renderLanes をスケジューリング優先度に変換します。
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
    /**
     * 対応する優先度で Scheduler に React のタスクをスケジュールします。
    */
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  // root 上のタスク優先度とタスクを更新し、次回スケジューリングを開始する際に取得できるようにします。
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

ensureRootIsScheduled を分析すると、バッチ処理がどのように実現されているかについての大まかな理解が得られます。実際には、React 内の同じ優先度のタスクを再利用することによって実現されており、これにより hooks の dispatch を複数回トリガーしても、1 回だけレンダリングされることが可能になります。

これで、React Scheduler: Scheduler ソースコード分析 と組み合わせると、React 内の三大モジュール(スケジューリング → 調整 → レンダリング)のスケジューリング部分についてすべて紹介しました。次に、1 つの質問を通じてこの記事全体をつなげてみましょう。

React は useState の dispatch をトリガーしてからページにレンダリングするまでに何を行ったか —— スケジューリング編

  1. 対応する優先度の update インスタンスを生成し、useState に対応する hooks インスタンスの queue 属性に連結リスト形式でマウントします。
  2. 現在の update がページにレンダリングされる必要があると判断されると、この update の lane を現在の fiber ノードからその親ノードの childLanes に向かって層層マークします(ルートまで)。
  3. その後、scheduleUpdateOnFiber を呼び出してスケジューリング段階に入ります。
    1. まず、現在の update の lanes を root.pendingLanes にマークします。pendingLanes 上のタスクはすべて待機中のタスクと見なされます。
    2. 次に、pendingLanes 内の最も高い優先度を現在の renderLanes として取得します。
    3. Scheduler にスケジューリングタスクを登録する前に、既に登録されているがまだ実行されていないタスク(root.callbackNode)が存在するかどうかを確認します。もし存在すれば、そのタスクの優先度と renderLanes を比較します。
      1. 優先度が同じであれば、そのタスクを再利用します。
      2. renderLanes の方が高ければ、旧タスク(root.callbackNode)をキャンセルし、新たに Scheduler 内でタスクを再登録し、返されたタスクを root.callbackNode に割り当てます。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。