説明#
- useCallback と useMemo は最適化のためのフックであり、React コンポーネント内の不必要なレンダリングや計算を減らすのに役立ちますが、本記事ではこれらのフックの実装のみを分析し、使用方法については説明しません。バージョンは v18.1.0 に基づいています。
 - 本文を読む前に React Hooks: hooks リンクリスト を先に読んでください。
 - 他のほとんどのフックと同様に、各フックはマウントとアップデートの 2 つの段階で説明されます。
 - 分析は以下のコードに基づいています。
 
import { useCallback, useEffect, useMemo, useState } from "react";
function UseCallbackAndUseMemo() {
  const [a, setA] = useState('a');
  const [b] = useState('b');
  const memoizedCallback = useCallback(
    () => {
      console.log(a, b);
    },
    [a, b],
  );
  const memoizedResult = useMemo(() => {
    return {
      a,
      b
    }
  }, [a, b]);
  useEffect(() => {
    setTimeout(() => {
      setA('aaa');
    }, 3000);
  }, []);
  useEffect(() => {
    console.log(memoizedResult, 'memoizedResult change');
  }, [
    memoizedResult
  ]);
  return (
    <div onClick={ memoizedCallback }>ここをクリック</div>
  )
}
export default UseCallbackAndUseMemo;
useCallback#
マウント段階の useCallback#
useCallback にブレークポイントを設定すると、mountCallback に入ります。

中に何があるか見てみましょう。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // フックオブジェクトを生成する、フックリンクリスト内の分析を参照
  const hook = mountWorkInProgressHook();
  // 値を設定
  const nextDeps = deps === undefined ? null : deps;
  // 関数と依存関係をフックオブジェクトの memoizedState に保存
  hook.memoizedState = [callback, nextDeps];
  // 関数を返す
  return callback;
}
それだけです。。。では、アップデートで何が行われたか見てみましょう。
アップデート段階の useCallback#
ブレークポイントをそのままにして、タイマーが実行されるのを待つと、updateCallback に入ります。

中に何があるか見てみましょう。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // フックリンクリストのポインタを移動し、現在の useCallback に対応するフックオブジェクトを指すようにする
  const hook = updateWorkInProgressHook();
  // アップデート段階で渡された依存関係を取得
  const nextDeps = deps === undefined ? null : deps;
  // 前回存在したフックオブジェクトの [callback, deps] を取得
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      /**
       * prevState が空でない場合(空になることはないが)
       * nextDeps も空でない場合に入る
      */
      // memoizedState 内の依存項目(deps)を取得
      const prevDeps: Array<mixed> | null = prevState[1];
      // 前後の依存関係が同じか比較
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 同じなら前回キャッシュされた関数を返す
        return prevState[0];
      }
    }
  }
  // 上記の条件が満たされない場合、memoizedState 内のデータを更新
  hook.memoizedState = [callback, nextDeps];
  // 関数を返す
  return callback;
}
大まかに言えば、前後の依存項目を比較して、前回キャッシュされた関数を再利用するかどうかを決定する、クローズのような実装です。例を挙げると、以下のコードのようになります。
  function exampleA(a) {
    let number = a;
    return function example() {
      return a + 2
    }
  }
  function exampleB(b) {
    let number = b;
    return function example() {
      return b + 2
    }
  }
  const func1 = exampleA(2);
  const func2 = exampleB(2);
両方の関数に同じ値を渡すと、実行結果はどちらも 4 になりますが、func1 と func2 は同じ関数ではありません。React のコンポーネントに渡すと、必ず props が変化し、コンポーネントが再レンダリングされますが、再レンダリング後もページは変化しません。したがって、不必要な再レンダリングを防ぐために、実行結果が同じキャッシュ関数を使用します。
次に、areHookInputsEqual が前後の依存関係をどのように比較するか見てみましょう。
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }
  // 両方の配列を同時に走査し、一つ一つ比較
  // is は浅い比較 ===
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
useCallback のまとめ#
useCallback は関数と依存関係を対応するフックオブジェクトに保存し、更新時に前後の依存関係を一つ一つ浅く比較して関数が再利用できるかどうかを判断し、関数の再利用を実現し、コンポーネントの不必要な更新を避けます。
useMemo#
マウント段階の useMemo#
useMemo にブレークポイントを設定すると、mountMemo に入ります。

/**
 *  useCallBack のマウント段階とほぼ同じロジック
*/
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 唯一の違いはここ、useMemo は関数実行後の結果を保存する
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
アップデート段階の useMemo#
ブレークポイントをそのままにして、タイマーが実行されるのを待つと、updateMemo に入ります。
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // これらが定義されていると仮定します。そうでない場合、areHookInputsEqual が警告を出します。
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
useCallback のアップデート段階と全く同じロジックです。これ以上は説明しません。
useMemo のまとめ#
useMemo は useCallback の実装とほぼ一致しており、唯一の違いは、useMemo が関数の実行結果を保存し、useCallback が関数そのものを保存することです。