banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useEffectとuseLayoutEffect

TLNR#

私はこの記事が読者が以下の点を理解するのに役立つことを願っています:

  1. use (layout) Effect が呼び出されると effct オブジェクトが作成され、そのオブジェクトはリンクリストの形式で fiber の updateQueue にマウントされます。
  2. useEffect は Scheduler によって NormalSchedulerPriority でスケジュールされるため、非同期操作です。
  3. useLayoutEffect の create は layout フェーズで同期的に処理され、その前の destroy は mutation フェーズで同期的に処理されるため、この hook 内で時間のかかる操作を呼び出すとページの更新がブロックされます。

読む前に知っておくべきこと#

  • useEffect は Scheduler との相互作用に関わるため、その実行順序を理解するには React のスケジューリングの動作について一定の理解が必要です。簡単に言えば、次のように理解できます(厳密な言い方ではなく、意訳として受け取ってください)。
    • Scheduler に scheduleCallback を通じて登録されたすべてのタスクは非同期タスクであり、そのタスクのタイプはTaskです。
    • React 内部には高優先度の割り込みメカニズムがあり、useEffect のスケジューリングは中断される可能性があり、期待とは異なる状況が発生することがあります。

説明#

引き続きReact hooks: hooks のリンクリスト内のサンプルコードを使用します。

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

    useEffect(() => {
      console.log('useEffect create');
      
      return () => {
        console.log('useEffect destroy');
      }
    }, [text]);

    useLayoutEffect(() => {
      console.log('useLayoutEffect create');

      return () => {
        console.log('useLayoutEffect destroy');
      }
    }, [text]);

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

以前のデバッグ経験から、各 hook が呼び出されると mountWorkInProgressHook が呼び出され、hook オブジェクトが作成され、呼び出し順序に基づいてリンクリスト構造が形成され、その hook リンクリストは対応する fiber オブジェクトの memoizedState 属性にマウントされます。

さらに、異なるタイプの hook のロジックは異なるため、ここでは useEffect のみを分析します。しかし、その前にその構造の呼称について合意を形成します。

  use(Layout)Effect(() => { // 最初の引数をcreateと定義
    return () => { // create関数の戻り値をdestroyと定義

    }
  }, []); // 2番目の引数を依存関係と定義

effect リンクリスト#

effect リンクリストの構築は beginWork 内で発生します。

image

hook はコンポーネントがマウントされているか更新されているかに応じて異なるロジックを実行します。use (Layout) Effect の場合、マウント時には mount (Layout) Effect が呼び出され、更新時には update (Layout) Effect が呼び出されます。

マウント時に対応する effect リンクリストを生成#

例の useEffect にブレークポイントを設定すると、mountEffect 内の mountEffectImpl に入ります。

  mountEffectImpl(
    UpdateEffect | PassiveEffect, // React内の権限に関する多くのビット操作が存在し、ここでの**または**操作は権限を付与する意味を持つことが多い
    HookPassive,
    create,
    deps,
  );

  function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
    /**
     * hookリンクリストは分析済みのためスキップ
    */
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    /**
     * A |= B => A = A | B
    */
    currentlyRenderingFiber.flags |= fiberFlags;
    /**
     * ここでのロジックは重要で、effectオブジェクトの作成とリンクリストの接続に関わります
     * また、useEffectに対応するhookオブジェクトのmemoizedStateにはeffectリンクリストがマウントされます
     * useLayoutEffectのロジックはuseEffectと大体同じですが、effectオブジェクトには異なるラベルが付けられます
    */
    hook.memoizedState = pushEffect(
      HookHasEffect | hookFlags,
      create,
      undefined,
      nextDeps,
    );
  }

pushEffect は effect リンクリストを構成する鍵であり、その内部で生成された effect 環状リンクリストは現在のコンポーネントの fiber の updateQueue にマウントされ、updateQueue.lastEffect は最新に生成された effect のオブジェクトを指します。

image

effect オブジェクトの構造は以下の通りです。

  const effect: Effect = {
    tag,
    create, // use(Layout)Effectのcreate
    destroy, // use(Layout)Effectのdestroy
    deps, // use(Layout)Effectの依存関係
    // Circular
    next: (null: any), // 次のeffectを指す
  };

mountEffectImpl を実行した後、生成された effect リンクリストは 2 か所にマウントされ、useEffect もこの時点で実行を終了します。

  1. use (Layout) Effect に対応する hook 要素の memoizedState
  2. fiber.updateQueue

次に実行される useLayoutEffect も上記の手順に従い、effect リンクリストは以下のようになります。

image

実際にそうです。

image

これでマウントフェーズでの effect リンクリスト生成に関するロジックは終了しました。

更新時に対応する effect リンクリストを生成#

元のブレークポイントを保持し、クリックコールバックをトリガーすると、updateEffect の updateEffectImpl に入ります。

  updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  )

  function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
    const hook = updateWorkInProgressHook(); // 更新の場合、currentHookに値を割り当てます
    const nextDeps = deps === undefined ? null : deps;
    let destroy = undefined;

    if (currentHook !== null) {
      const prevEffect = currentHook.memoizedState; // currentHookに対応するeffectオブジェクトを取得
      destroy = prevEffect.destroy; // 前回のdestroy関数をwipに割り当て、今回呼び出します
      if (nextDeps !== null) {
        const prevDeps = prevEffect.deps;
        if (areHookInputsEqual(nextDeps, prevDeps)) { // 新旧のdepを一つずつ比較しますが、浅い比較です
          pushEffect(hookFlags, create, destroy, nextDeps);
          return;
        }
      }
    }

    currentlyRenderingFiber.flags |= fiberFlags;

    hook.memoizedState = pushEffect(
      HookHasEffect | hookFlags, // HookHasEffectは更新をマークします
      create,
      destroy,
      nextDeps,
    );
  }

マウントとは異なり、update の pushEffect は前回の destroy 関数を持ちます。

useEffect 内の create と destroy の実行順序について#

この時点で useEffect の create 関数にブレークポイントを設定して、その呼び出しスタックを確認します。

image

ここで flushPassiveEffectsImpl という関数に注目します。この関数は flushPassiveEffects 内で NormalSchedulerPriority レベルの優先度でスケジュールされるため、flushPassiveEffectsImpl は非同期タスクであり、優先度は高くありません。しかし、現在の例では割り込みの状況を考慮する必要はありません。

flushPassiveEffects を全体的に検索することで(手がかりは pendingPassiveHookEffectsMount の割り当て)、最終的に呼び出し位置を commitBeforeMutationEffects に特定しました。

非同期操作の呼び出しを調査するのは非常に面倒ですが、最終的には commitRoot ——> commitRootImpl ——> commitBeforeMutationEffects に特定しました。この関数は BeforeMutation フェーズのエントリです。

effect の収集#

flushPassiveEffectsImpl が effect をクリアする前に、まず fiber 上の effect リンクリストを収集する必要があります。この操作は schedulePassiveEffects 内で発生します。schedulePassiveEffects は commitLayoutEffects ——> commitLifeCycles 内で呼び出されるため、layout フェーズ内で呼び出されると見なすことができます。

  /**
   * ここでのfinishedWorkはUseEffectAnduseLayoutEffectのfiberオブジェクトです
  */
  function schedulePassiveEffects(finishedWork: Fiber) {
    /**
     * fiberのupdateQueueには対応するeffectリンクリストがマウントされています
    */
    const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
    const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
      // lastEffectこのポインタは最後のeffectオブジェクトを指しているため、nextを通じて最初の要素を直接取得できます
      const firstEffect = lastEffect.next;
      let effect = firstEffect;
      do {
        const {next, tag} = effect;
        if (
          (tag & HookPassive) !== NoHookEffect && // use(Layout)Effectによって作成されたeffectオブジェクト以外をフィルタリング
          (tag & HookHasEffect) !== NoHookEffect // 依存関係が変更されていないeffectオブジェクトをフィルタリング
        ) {
          enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); // pendingPassiveHookEffectsUnmountに値を割り当て
          enqueuePendingPassiveHookEffectMount(finishedWork, effect);// pendingPassiveHookEffectsMountに値を割り当て
          // 現在、pendingPassiveHookEffectsUnmountとpendingPassiveHookEffectsMountのデータは一致しています
        }
        effect = next;
      } while (effect !== firstEffect); // 環状リンクリストを一周して停止
    }
  }

effect の実行#

flushPassiveEffectsImpl を直接デバッグし、以下のコードから余分な部分を削除しました。この関数内部では effect オブジェクトの create と destroy の実行ロジックが含まれています。

function flushPassiveEffectsImpl() {
  ...
  /**
   * effectのdestroy関数を実行します
   * マウント時には、pushEffectを実行する際に第三引数がundefinedであるため、effectオブジェクトのdestroy属性は空です。したがって、destroyは実行されません。
   * しかし、更新時にはcreateから返されたdestroy関数が渡されるため、実行されます。以下で言及します。
  */
  const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i += 2) {
    const effect = ((unmountEffects[i]: any): HookEffect);
    const fiber = ((unmountEffects[i + 1]: any): Fiber);
    const destroy = effect.destroy;
    effect.destroy = undefined;

    if (typeof destroy === 'function') {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        fiber.mode & ProfileMode
      ) {
        try {
          startPassiveEffectTimer();
          destroy();
        } finally {
          recordPassiveEffectDuration(fiber);
        }
      } else {
        destroy();
      }
    }
  }
  /**
   * effectのcreate関数を実行します
  */
  const mountEffects = pendingPassiveHookEffectsMount;
  pendingPassiveHookEffectsMount = [];
  for (let i = 0; i < mountEffects.length; i += 2) {
    const effect = ((mountEffects[i]: any): HookEffect);
    const fiber = ((mountEffects[i + 1]: any): Fiber);

    const create = effect.create;
    if (
      enableProfilerTimer &&
      enableProfilerCommitHooks &&
      fiber.mode & ProfileMode
    ) {
      try {
        startPassiveEffectTimer();
        /**
         * createの戻り値はdestroyであり、このeffectオブジェクトがupdateによって再度収集されると、
         * 次回のflushPassiveEffectsImplで指されます
        */
        effect.destroy = create(); 
      } finally {
        recordPassiveEffectDuration(fiber);
      }
    } else {
      effect.destroy = create();
    }

  }
  ...

  return true;
}

結論#

  1. useEffect は非同期操作であり、副作用をクリアする操作(flushPassiveEffectsImpl)はタスクとして登録されます。
  2. layout フェーズ内で effect は実行配列に収集されますが、スケジューリングは before Mutation で発生します。
  3. 副作用をクリアする操作(flushPassiveEffectsImpl)のスケジューリング優先度は高くないため、スケジューリング時により高い優先度のタスクに割り込まれる可能性があります。
  4. マウント時には create 関数のみが実行され、更新時には currentHook の destroy が実行された後、新しい hook の create が実行されます。

useLayoutEffect の実行タイミングについて#

useLayoutEffect はトリガータイミングを除いて、他の動作は useEffect と一致します。

この時点で useLayoutEffect の create 関数にブレークポイントを設定して、その呼び出しスタックを確認します。

image

commitHookEffectListMount にブレークポイントを設定すると、すべて同期操作であることがわかります。(commitLayoutEffects は layout のエントリです)

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 伝えられたtagはHookLayout | HookHasEffectであるため、useLayoutEffectのeffectをフィルタリングできます
      if ((effect.tag & tag) === tag) { 
        // マウント
        const create = effect.create;
        effect.destroy = create(); 
      }
      effect = effect.next;
    } while (effect !== firstEffect); // 一周します
  }
}

useLayoutEffect の destroy 関数にブレークポイントを設定すると

image

commitHookEffectListUnmount にブレークポイントを設定すると、すべて同期操作であることがわかります。(commitMutationEffects は mutation のエントリです)

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // アンマウント
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

結論#

  1. useLayoutEffect は同期的に呼び出され、その destroy は mutation フェーズで呼び出され、その create は layout フェーズで呼び出されます。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。