React useMemo 深度源码解析

25 阅读16分钟

React useMemo 深度源码解析

一、整体架构认知

┌─────────────────────────────────────────────────────┐
│                    React 应用                        │
│                                                     │
│   useMemo(fn, deps)                                 │
│        │                                            │
│        ▼                                            │
│   ReactCurrentDispatcher  ← 根据阶段切换 dispatcher │
│        │                                            │
│   ┌────┴─────────────┐                              │
│   │                  │                              │
│   ▼                  ▼                              │
│ mount阶段         update阶段                        │
│ mountMemo()      updateMemo()                       │
│   │                  │                              │
│   ▼                  ▼                              │
│ 执行fn,存结果    比较deps                            │
│ 存到hook.memo    │                                  │
│ State上          ├─ 相同 → 返回缓存值               │
│                  └─ 不同 → 重新执行fn                │
│                                                     │
│          底层存储: Fiber.memoizedState               │
│          (hook 单向链表)                              │
└─────────────────────────────────────────────────────┘

二、从调用入口开始

1. useMemo 的入口定义

// packages/react/src/ReactHooks.js

/**
 * useMemo 的对外API入口
 * 它本身不包含任何逻辑,只是一个"转发器"
 * 真正的实现取决于当前的 dispatcher
 */
function useMemo<T>(
  create: () => T,    // 工厂函数,返回需要缓存的值
  deps: Array<mixed> | void | null  // 依赖数组
): T {
  // 获取当前的 dispatcher
  // 这是 React Hooks 能工作的关键机制
  const dispatcher = resolveDispatcher();
  
  // 委托给 dispatcher 上的 useMemo
  return dispatcher.useMemo(create, deps);
}

2. Dispatcher 切换机制

// packages/react-reconciler/src/ReactFiberHooks.js

/**
 * React 内部维护了一个全局变量 ReactCurrentDispatcher
 * 在组件渲染的不同阶段,会被赋予不同的 dispatcher 对象
 * 
 * 这就是为什么 hooks 只能在组件函数体内调用:
 * 只有在渲染过程中,dispatcher 才会被正确设置
 */

// ====== 不同阶段的 dispatcher 对象 ======

// 首次渲染(mount)时的 dispatcher
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  useMemo: mountMemo,        // ← mount 阶段
  useCallback: mountCallback,
  useRef: mountRef,
  // ...其他hooks
};

// 更新(update)时的 dispatcher
const HooksDispatcherOnUpdate = {
  useState: updateState,
  useEffect: updateEffect,
  useMemo: updateMemo,       // ← update 阶段
  useCallback: updateCallback,
  useRef: updateRef,
  // ...其他hooks
};

// 非法调用时的 dispatcher(开发环境下会报错提示)
const ContextOnlyDispatcher = {
  useMemo: throwInvalidHookError,
  // ... 所有 hooks 都指向报错函数
};

/**
 * 在 renderWithHooks 中切换 dispatcher
 * 这是每次组件渲染的入口函数
 */
function renderWithHooks(
  current: Fiber | null,        // 当前屏幕上的 fiber(null 表示首次渲染)
  workInProgress: Fiber,        // 正在构建的新 fiber
  Component: Function,          // 组件函数
  props: any,
  secondArg: any,
  nextRenderLanes: Lanes
): any {
  // 保存当前正在渲染的 fiber
  currentlyRenderingFiber = workInProgress;
  
  // 重置 hook 链表(重要!每次渲染都从头开始遍历)
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;

  // ★★★ 关键:根据是否有 current fiber 来判断mount/update ★★★
  if (current !== null && current.memoizedState !== null) {
    // 更新阶段:fiber 已存在,有之前的 hook 状态
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    // 挂载阶段:首次渲染
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }

  // ★ 执行组件函数 — 此时组件内的 hooks 调用会走对应的 dispatcher
  let children = Component(props, secondArg);

  // 渲染完毕,切回非法 dispatcher(防止在组件外调用 hooks)
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  // 重置全局变量
  currentlyRenderingFiber = null;
  currentHook = null;
  workInProgressHook = null;

  return children;
}

三、Fiber 上的 Hook 数据结构

/**
 * ====== 核心数据结构 ======
 * 
 * 每个 hook 调用对应一个 Hook 对象
 * 所有 Hook 对象通过 next 指针串成单向链表
 * 链表挂在 Fiber.memoizedState 上
 */

// Hook 对象的结构
type Hook = {
  memoizedState: any,    // 存储 hook 的状态/缓存值
  baseState: any,        // 基础状态(用于 useState/useReducer)
  baseQueue: Update<any> | null,
  queue: UpdateQueue<any> | null,
  next: Hook | null,     // 指向下一个 hook ← 形成链表
};

/**
 * 直观理解 Hook 链表:
 * 
 * function MyComponent() {
 *   const [count, setCount] = useState(0);      // Hook1
 *   const doubled = useMemo(() => count*2, [count]); // Hook2
 *   const ref = useRef(null);                    // Hook3
 *   useEffect(() => { ... }, [count]);           // Hook4
 *   return <div>{doubled}</div>;
 * }
 * 
 * Fiber.memoizedState 指向:
 * 
 * Hook1 (useState) ──next──→ Hook2 (useMemo) ──next──→ Hook3 (useRef) ──next──→ Hook4 (useEffect) ──next──→ null
 *   │                          │
 *   │ memoizedState: 0         │ memoizedState: [cachedValue, deps]
 *   
 * ⚠️ 这就是为什么 hooks 不能放在条件语句中!
 *    因为每次渲染必须保证 hook 链表的顺序一致
 *    React 靠"调用顺序"来匹配新旧 hook
 */
Fiber 节点
┌──────────────────────────────┐
 type: MyComponent            
 memoizedState ───────────┐   
 ...                         
└──────────────────────────┼───┘
                           
                    ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
                       Hook #1   │     │   Hook #2   │     │   Hook #3   │
                      useState   │────→│  useMemo    │────→│  useRef     │──→ null
                                                                     
                     memoized:         memoized:         memoized:   
                       0               [value,deps]      {current:x} 
                    └─────────────┘     └─────────────┘     └─────────────┘

四、Mount 阶段源码 — mountMemo

// packages/react-reconciler/src/ReactFiberHooks.js

/**
 * mountMemo - 首次渲染时 useMemo 的实现
 * 
 * 职责:
 * 1. 创建新的 hook 节点并加入链表
 * 2. 执行工厂函数得到初始值
 * 3. 将值和依赖数组一起缓存到 hook.memoizedState
 */
function mountMemo<T>(
  nextCreate: () => T,           // 工厂函数
  deps: Array<mixed> | void | null  // 依赖数组
): T {
  // ★ 第1步:创建 hook 对象并挂到链表上
  const hook = mountWorkInProgressHook();

  // 处理 deps(undefined 和 null 有不同含义)
  const nextDeps = deps === undefined ? null : deps;

  // ★ 第2步:标记当前 Fiber 允许在 render 阶段有副作用
  // 这是 React 19 新增的,用于 useMemo 的开发环境 double-invoke
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate(); // 开发环境下多调用一次,检测副作用
  }

  // ★ 第3步:执行工厂函数,得到需要缓存的值
  const nextValue = nextCreate();

  // ★ 第4步:将 [值, 依赖] 存到 hook.memoizedState
  // 这是 useMemo 的核心存储格式:一个包含两个元素的数组
  hook.memoizedState = [nextValue, nextDeps];

  // 返回计算出的值
  return nextValue;
}

/**
 * mountWorkInProgressHook - 创建新 hook 并接入链表
 * 
 * 这是所有 hooks 在 mount 阶段共用的创建函数
 */
function mountWorkInProgressHook(): Hook {
  // 创建新的 hook 对象
  const hook: Hook = {
    memoizedState: null,   // 将存储 [value, deps]
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,            // 链表指针
  };

  if (workInProgressHook === null) {
    // ★ 情况1:这是第一个 hook
    // 将它设为 Fiber 的 memoizedState(链表头)
    currentlyRenderingFiber.memoizedState = hook;
    workInProgressHook = hook;
  } else {
    // ★ 情况2:已有前置 hook,接到链表尾部
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }

  return hook;
}

Mount 过程图解

调用 useMemo(() => expensiveCalc(a, b), [a, b])
                    │
                    ▼
           mountMemo(create, deps)
                    │
     ┌──────────────┼──────────────┐
     ▼              ▼              ▼
  创建Hook      执行create()    存储结果
     │              │              │
     ▼              ▼              ▼
  hook = {       nextValue =    hook.memoizedState = 
    memoized-    create()        [nextValue, deps]
    State:null   // 执行计算      ↑
    next: null                   这个数组就是缓存
  }                              [计算结果, [a, b]]
     │
     ▼
  接入Fiber链表
  fiber.memoizedState → hook1 → hook2(useMemo) → ...

五、Update 阶段源码 — updateMemo

/**
 * updateMemo - 更新渲染时 useMemo 的实现
 * 
 * 核心逻辑:
 * 1. 取出上一次缓存的 [value, deps]
 * 2. 浅比较新旧 deps
 * 3. 相同则返回缓存值,不同则重新计算
 */
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null
): T {
  // ★ 第1步:获取当前 hook(从链表中按顺序取出)
  const hook = updateWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;

  // ★ 第2步:取出上一次的缓存值
  const prevState = hook.memoizedState;
  // prevState = [previousValue, previousDeps]

  if (nextDeps !== null) {
    // ★ 第3步:取出上一次的依赖数组
    const prevDeps: Array<mixed> | null = prevState[1];

    // ★ 第4步:逐个浅比较依赖项(核心!)
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // ✅ 依赖没变,直接返回缓存的值
      // 不执行 nextCreate(),这就是"跳过计算"的关键
      return prevState[0];
    }
  }

  // ❌ 依赖变了(或没有提供deps),需要重新计算

  // 开发环境 double invoke 检测
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
  }

  // ★ 第5步:执行工厂函数得到新值
  const nextValue = nextCreate();

  // ★ 第6步:更新缓存
  hook.memoizedState = [nextValue, nextDeps];

  return nextValue;
}

/**
 * updateWorkInProgressHook - 从旧链表中复用/创建 hook
 * 
 * 这是 update 阶段所有 hooks 共用的
 * 关键作用:按顺序从旧 fiber 的 hook 链表中取出对应的 hook
 * 
 * 这也是为什么 hook 顺序不能变的根本原因:
 * 第N次调用 updateWorkInProgressHook 就取出链表第N个节点
 */
function updateWorkInProgressHook(): Hook {
  // ★ nextCurrentHook 指向旧 fiber 链表中的下一个 hook
  let nextCurrentHook: Hook | null;

  if (currentHook === null) {
    // 第一个 hook,从旧 fiber 的链表头开始
    const current = currentlyRenderingFiber.alternate; // alternate 指向旧 fiber
    if (current !== null) {
      nextCurrentHook = current.memoizedState; // 旧 fiber 的 hook 链表头
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 后续 hook,沿链表向下走
    nextCurrentHook = currentHook.next;
  }

  // 对应到新 fiber 链表
  let nextWorkInProgressHook: Hook | null;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 已经存在的 hook(re-render 场景)
    workInProgressHook = nextWorkInProgressHook;
    currentHook = nextCurrentHook;
  } else {
    // ★ 从旧 hook 克隆一个新 hook
    if (nextCurrentHook === null) {
      // 不应该出现的情况,说明 hook 数量发生了变化
      // 这就是条件调用 hooks 会出错的根本原因!
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error('...');
      } else {
        throw new Error(
          'Rendered more hooks than during the previous render.'
        );
      }
    }

    currentHook = nextCurrentHook;

    // 创建新 hook,复制旧 hook 的数据
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState, // ← 复制缓存
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    // 接入新链表
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = newHook;
      workInProgressHook = newHook;
    } else {
      workInProgressHook.next = newHook;
      workInProgressHook = newHook;
    }
  }

  return workInProgressHook;
}

Update 过程图解

重新渲染时调用 useMemo(() => expensiveCalc(a, b), [a, b])
                         │
                         ▼
                  updateMemo(create, [a, b])
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
     从链表取出对应Hook         比较依赖数组
     (updateWorkInProgress-    areHookInputsEqual(
      Hook)                     [a, b],    ← 新deps  
              │                 [a_old, b_old])← 旧deps
              ▼                     │
     hook.memoizedState =      ┌───┴───┐
     [oldValue, [a_old,b_old]] │       │
                            相同     不同
                               │       │
                               ▼       ▼
                          返回旧值   执行create()
                          oldValue   newValue
                               │       │
                               │       ▼
                               │   hook.memoizedState =
                               │   [newValue, [a, b]]
                               │       │
                               ▼       ▼
                            返回给组件使用

六、依赖比较算法 — areHookInputsEqual

/**
 * areHookInputsEqual - 所有 hooks 共用的依赖比较函数
 * 
 * 这是 useMemo、useCallback、useEffect 判断是否需要更新的核心
 * 使用的是 Object.is 浅比较
 */
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null
): boolean {

  // ★ 情况1:上一次没有 deps(不应该出现,但做兼容)
  if (prevDeps === null) {
    // 开发环境下会有 warning
    if (__DEV__) {
      console.error(
        '%s received a final argument during this render, ' +
        'but not during the previous render. ' +
        'Even though the final argument is optional, ' +
        'its type cannot change between renders.',
        currentHookNameInDev
      );
    }
    return false; // 视为不相等,重新计算
  }

  // ★ 情况2:开发环境检查 deps 长度变化
  if (__DEV__) {
    if (nextDeps.length !== prevDeps.length) {
      console.error(
        'The final argument passed to %s changed size between renders. ' +
        'The order and size of this array must remain constant.\n\n' +
        'Previous: [%s]\n' +
        'Incoming: [%s]',
        currentHookNameInDev,
        prevDeps.join(', '),
        nextDeps.join(', ')
      );
    }
  }

  // ★ 情况3:逐项使用 Object.is 比较
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // 使用 Object.is 进行比较
    if (is(nextDeps[i], prevDeps[i])) {
      continue;  // 这一项相同,继续检查下一项
    }
    // 发现不同,整体视为不相等
    return false;
  }

  // 所有项都相同
  return true;
}

/**
 * Object.is 的polyfill
 * 与 === 的区别:
 * - Object.is(NaN, NaN)  → true  (=== 返回 false)
 * - Object.is(+0, -0)    → false (=== 返回 true)
 */
function is(x: any, y: any): boolean {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || 
    (x !== x && y !== y) // NaN check
  );
}

比较过程图解

areHookInputsEqual([newA, newB, newC], [oldA, oldB, oldC])

  i=0: Object.is(newA, oldA) → truecontinue
  i=1: Object.is(newB, oldB) → truecontinue
  i=2: Object.is(newC, oldC) → falsereturn false
  
  结果:不相等 → 重新执行 create 函数


Object.is 比较规则:
┌──────────────────────────────────────────┐
│ Object.is(1, 1)              → true      │
│ Object.is('a', 'a')         → true      │
│ Object.is(null, null)       → true      │
│ Object.is(obj, obj)         → true  (同一个引用)  │
│ Object.is(obj, {...obj})    → false (不同引用!) │
│ Object.is([1,2], [1,2])    → false (不同引用!) │
│ Object.is(NaN, NaN)        → true  (特殊)│
│ Object.is(+0, -0)          → false (特殊)│
└──────────────────────────────────────────┘

⚠️ 这意味着:
  每次渲染创建新对象/数组作为dep → 永远不相等 → useMemo 永远失效!

七、React Compiler(React 19 Forget)中的 useMemo

/**
 * React Compiler 会自动分析代码并插入缓存逻辑
 * 理解编译器做了什么有助于理解 useMemo 的本质
 */

// ====== 你写的代码 ======
function ProductPage({ productId, referrer }) {
  const product = useProduct(productId);
  
  // 你需要手动写 useMemo 来避免重新计算
  const visibleProducts = useMemo(
    () => filterProducts(products, category),
    [products, category]
  );
  
  return <ProductList products={visibleProducts} />;
}

// ====== React Compiler 编译后(概念性)======
function ProductPage({ productId, referrer }) {
  const product = useProduct(productId);
  
  // 编译器自动生成的缓存逻辑(使用内部 hook)
  // 类似 useMemoCache —— 编译器专用的缓存 hook
  const $ = useMemoCache(4); // 申请4个缓存槽位
  
  let visibleProducts;
  
  // 槽位0: 缓存 products
  // 槽位1: 缓存 category
  // 槽位2: 缓存 filterProducts 的结果
  if ($[0] !== products || $[1] !== category) {
    visibleProducts = filterProducts(products, category);
    $[0] = products;
    $[1] = category;
    $[2] = visibleProducts;
  } else {
    visibleProducts = $[2];
  }

  // 槽位3: 缓存 JSX 元素
  let t0;
  if ($[3] !== visibleProducts) {
    t0 = <ProductList products={visibleProducts} />;
    $[3] = visibleProducts;
  } else {
    t0 = $[3]; // 复用缓存的 JSX
  }
  
  return t0;
}

/**
 * useMemoCache — React 编译器使用的内部 hook
 * 
 * 与 useMemo 的区别:
 * 1. useMemo: 一个 hook 缓存一个值
 * 2. useMemoCache: 一个 hook 提供多个缓存槽位
 *    编译器将整个组件的所有缓存需求合并到一个缓存数组中
 */
function useMemoCache(size: number): Array<any> {
  const hook = updateWorkInProgressHook();
  
  let memoCache = hook.memoizedState;
  if (memoCache == null) {
    // 初始化缓存数组
    memoCache = {
      data: new Array(size).fill(MEMO_CACHE_SENTINEL),
      index: 0,
    };
    hook.memoizedState = memoCache;
  }
  
  return memoCache.data;
}

八、useMemo vs useCallback 源码对比

/**
 * useMemo 和 useCallback 的实现几乎完全一样
 * 唯一的区别是:缓存的是"返回值"还是"函数本身"
 */

// ====== useMemo: 缓存值 ======
function mountMemo<T>(nextCreate: () => T, deps): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();            // ★ 执行函数,拿到返回值
  hook.memoizedState = [nextValue, nextDeps]; // ★ 缓存 返回值
  return nextValue;                           // ★ 返回 值
}

// ====== useCallback: 缓存函数 ======
function mountCallback<T>(callback: T, deps): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // ★ 注意区别:没有执行 callback!直接缓存函数本身
  hook.memoizedState = [callback, nextDeps];  // ★ 缓存 函数
  return callback;                            // ★ 返回 函数
}

/**
 * 所以:
 * useCallback(fn, deps)  ≡  useMemo(() => fn, deps)
 * 
 * useCallback 只是 useMemo 的语法糖!
 */

// ====== update 阶段对比 ======

function updateMemo<T>(nextCreate: () => T, deps): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的 值
    }
  }
  const nextValue = nextCreate();  // ★ 重新执行
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的 函数
    }
  }
  // ★ 不执行,直接存新函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

九、实际案例与常见陷阱

陷阱1:依赖项是新对象引用

function BadExample({ items }) {
  // ❌ 每次渲染 filter 都返回新数组引用
  // Object.is([], []) === false
  // 所以 useMemo 每次都重新计算,完全失效!
  const filtered = useMemo(
    () => items.filter(i => i.active),
    [items.filter(i => i.active)]  // ← 新数组!每次都不相等
  );

  // ✅ 正确做法:依赖原始数据
  const filtered = useMemo(
    () => items.filter(i => i.active),
    [items]  // ← items 引用没变就不重算
  );
}

陷阱2:deps 为空/undefined/null 的区别

function DepsDemo() {
  // 情况1: 有依赖数组 → 正常的缓存行为
  const a = useMemo(() => compute(), [x, y]);

  // 情况2: 空依赖数组 → 只计算一次,永远返回缓存值(类似常量)
  const b = useMemo(() => compute(), []);

  // 情况3: 没有第二个参数(undefined)→ 每次渲染都重新计算!
  // useMemo 完全失去意义
  const c = useMemo(() => compute());

  // 情况4: 显式传 null → 和 undefined 一样,每次都重新计算
  const d = useMemo(() => compute(), null);
}

从源码看为什么

/**
 * 追踪 deps = undefined 的执行路径
 */

// ====== mount 阶段 ======
function mountMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook();
  
  // undefined → null
  const nextDeps = deps === undefined ? null : deps;
  // nextDeps = null
  
  const nextValue = nextCreate();
  
  // 存储:[value, null]
  hook.memoizedState = [nextValue, null];
  //                              ↑ deps 是 null
  
  return nextValue;
}

// ====== update 阶段 ======
function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  
  // 又是 undefined → null
  const nextDeps = deps === undefined ? null : deps;
  // nextDeps = null
  
  const prevState = hook.memoizedState;
  // prevState = [oldValue, null]
  
  // ★ 关键!nextDeps 是 null,跳过整个 if 块!
  if (nextDeps !== null) {         // null !== null  → false
    const prevDeps = prevState[1]; // 永远不会执行到这里
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];         // 永远不会执行到这里
    }
  }
  
  // ★ 直接到这里:每次都重新执行!
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

/**
 * 而 deps = [] 的执行路径:
 */
function updateMemo_emptyDeps(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  
  const nextDeps = []; // 空数组,但不是 null
  
  const prevState = hook.memoizedState;
  // prevState = [oldValue, []]
  
  // ★ [] !== null → true,进入 if
  if (nextDeps !== null) {
    const prevDeps = prevState[1]; // []
    
    // areHookInputsEqual([], [])
    // for循环遍历 0 个元素 → 直接返回 true
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0]; // ★ 永远返回缓存值!
    }
  }
  
  // 永远不会执行到这里
}

三种情况的完整流程图

useMemo(create, deps)
         │
         ▼
  nextDeps = (deps === undefined) ? null : deps
         │
         ├── deps = [a, b]          deps = []             deps = undefined/null
         │       │                     │                          │
         │       ▼                     ▼                          ▼
         │   nextDeps = [a,b]    nextDeps = []             nextDeps = null
         │       │                     │                          │
         │       ▼                     ▼                          ▼
         │  if(nextDeps!==null) if(nextDeps!==null)      if(nextDeps!==null)
         │     → TRUE ✓           → TRUE ✓                → FALSE ✗
         │       │                     │                          │
         │       ▼                     ▼                          │
         │  比较每一项dep         for循环0次                      │
         │  Object.is逐一对比     直接return true                │
         │       │                     │                          │
         │    ┌──┴──┐                  ▼                          │
         │    ▼     ▼            返回缓存值                       │
         │  相同   不同          (永不重算)                      │
         │    │     │                                             │
         │    ▼     ▼                                             ▼
         │ 返回   重新计算                                   每次都重新计算
         │ 缓存值  create()                                    create()
         │
         └── 这是正常使用场景

十、陷阱3:在 useMemo 内部产生副作用

// ❌ 错误:useMemo 中不应有副作用
function BadSideEffect() {
  const data = useMemo(() => {
    // 这些都不应该出现在 useMemo 中
    document.title = 'Updated';       // DOM操作
    localStorage.setItem('key', 'v'); // 存储操作
    console.log('computed!');         // 在Strict Mode下会double invoke
    fetchData();                      // 网络请求
    
    return expensiveCalc();
  }, [dep]);
}

源码层面的原因

/**
 * React 18 的 Strict Mode 和 React 19 的编译器
 * 都会在开发环境 double invoke create 函数
 */
function mountMemo<T>(nextCreate: () => T, deps): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;

  // ★ 开发环境下,额外调用一次 create 函数!
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();  // 第1次调用(结果被丢弃)
  }

  const nextValue = nextCreate();  // 第2次调用(使用这个结果)
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

/**
 * 为什么要 double invoke?
 * 
 * React 未来的 Concurrent 特性(如 Offscreen/Activity)
 * 可能会:
 * 1. 暂停渲染,稍后恢复
 * 2. 丢弃某次渲染结果重新来
 * 3. 同一次更新中多次调用 render
 * 
 * 如果 useMemo 中有副作用,这些场景下会出bug
 * double invoke 就是帮你提前发现这类问题
 * 
 * ┌──────────────────────────────────────┐
 * │  Concurrent Mode 下的渲染可能:      │
 * │                                      │
 * │  render开始 → 暂停 → 丢弃 → 重新render │
 * │       ↑                              │
 * │       这里如果有副作用就会执行多次!  │
 * └──────────────────────────────────────┘
 */

十一、陷阱4:过度使用 useMemo

// ❌ 没必要的 useMemo — 简单计算反而更慢
function OverMemoized({ firstName, lastName }) {
  // 字符串拼接非常快,useMemo 的开销比计算本身还大
  const fullName = useMemo(
    () => `${firstName} ${lastName}`,
    [firstName, lastName]
  );
  
  // ✅ 直接计算
  const fullName = `${firstName} ${lastName}`;
}

从源码分析 useMemo 的额外开销

/**
 * useMemo 每次渲染(即使命中缓存)都需要执行的操作:
 */
function updateMemo(nextCreate, deps) {
  // 开销1:从链表中取出 hook 对象
  //   - 涉及多个指针操作和条件判断
  const hook = updateWorkInProgressHook();  // ~10 行逻辑

  // 开销2:处理 deps
  const nextDeps = deps === undefined ? null : deps;

  // 开销3:取旧状态
  const prevState = hook.memoizedState;

  if (nextDeps !== null) {
    const prevDeps = prevState[1];
    
    // 开销4:逐个比较依赖项
    //   - 创建函数调用栈
    //   - 循环 + Object.is
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }

  // 如果没命中,还有:
  // 开销5:执行 create 函数
  // 开销6:创建新数组 [value, deps]
  // 开销7:写入 hook.memoizedState
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

/**
 * ★ 总结:useMemo 的"底价"
 * 
 * 即使缓存命中(最理想情况),每次渲染仍需:
 * 1. updateWorkInProgressHook() — 链表遍历、指针操作
 * 2. 取出 prevState
 * 3. areHookInputsEqual() — 函数调用 + 循环比较
 * 
 * 如果你缓存的计算比这些操作还简单(如字符串拼接、简单加减)
 * 那 useMemo 反而是负优化
 * 
 * ┌─────────────────────────────────────────┐
 * │ 适合 useMemo 的场景:                    │
 * │  ✅ 大数组的 filter/map/sort/reduce      │
 * │  ✅ 递归计算 / 深层遍历                  │
 * │  ✅ 创建复杂对象(作为子组件的 props)    │
 * │  ✅ 正则匹配大量文本                     │
 * │                                         │
 * │ 不适合 useMemo 的场景:                  │
 * │  ❌ 简单的算术运算                       │
 * │  ❌ 字符串拼接                           │
 * │  ❌ 基本类型的直接返回                   │
 * │  ❌ 每次 deps 都会变的计算               │
 * └─────────────────────────────────────────┘
 */

十二、陷阱5:引用稳定性问题

function ReferenceStability({ items, threshold }) {
  // ❌ 即使 items 没变,每次渲染都传入新的内联函数作为 filter
  // 但这里不影响 useMemo,因为 filter函数不在 deps 中
  const filtered = useMemo(() => {
    return items.filter(item => item.value > threshold);
  }, [items, threshold]); // deps 中的是 items 和 threshold

  // ★ 关键问题出在这里:
  // filtered 引用是否稳定?取决于 items/threshold 是否变化

  // 如果 filtered 作为子组件的 prop:
  return <ChildComponent data={filtered} />;
  // 当 items/threshold 没变时:
  //   filtered 引用不变 → ChildComponent 配合 React.memo 可以跳过渲染
  // 当 items/threshold 变了时:
  //   filtered 是新数组 → ChildComponent 必须重新渲染

  // ❌ 对比没有 useMemo 的情况:
  const filteredBad = items.filter(item => item.value > threshold);
  // 每次渲染都是新引用 → ChildComponent 的 React.memo 永远失效
}

十三、Re-render 阶段的特殊处理

/**
 * React 还有一个 re-render dispatcher
 * 当在 render 过程中触发了 setState(比如在 render 中调用 dispatch)
 * React 会立即重新渲染该组件
 * 
 * 在这个场景下,useMemo 走的是 rerenderMemo
 */

const HooksDispatcherOnRerender = {
  useMemo: updateMemo,     // 注意:re-render 时复用 updateMemo
  useState: rerenderState,
  useReducer: rerenderReducer,
  // ...
};

/**
 * 完整的 dispatcher 切换流程:
 * 
 *                renderWithHooks()
 *                      │
 *          ┌───────────┴────────────┐
 *          │                        │
 *    current === null?        current !== null?
 *          │                        │
 *          ▼                        ▼
 *  HooksDispatcherOnMount   HooksDispatcherOnUpdate
 *          │                        │
 *          └───────────┬────────────┘
 *                      │
 *                Component(props)  ← 执行组件函数
 *                      │
 *              render中触发了setState?
 *                      │
 *              ┌───────┴───────┐
 *              │               │
 *             否              是
 *              │               │
 *              ▼               ▼
 *          正常结束      切换到 HooksDispatcherOnRerender
 *                              │
 *                        再次执行 Component(props)
 *                              │
 *                        使用 updateMemo/rerenderState...
 */

十四、React 19 中 useMemo 的变化

/**
 * React 19 带来的变化:
 * 
 * 1. React Compiler(原 React Forget)
 *    - 自动插入 memoization,不再需要手写 useMemo
 *    - 使用 useMemoCache 代替多个独立的 useMemo
 * 
 * 2. 新的 cache() API(用于服务端组件)
 *    - 请求级别的缓存,与 useMemo 互补
 */

// ====== React 19 之前 ======
function ProductPage({ productId }) {
  const product = useProduct(productId);
  
  // 手动写 useMemo
  const recommendations = useMemo(
    () => computeRecommendations(product),
    [product]
  );
  
  // 手动写 useCallback
  const handleAdd = useCallback(
    () => addToCart(product),
    [product]
  );
  
  return (
    <div>
      {/* 手动写 React.memo */}
      <MemoizedRecommendations items={recommendations} />
      <MemoizedButton onClick={handleAdd} />
    </div>
  );
}

// ====== React 19 + Compiler 之后 ======
// 编译器自动处理,你只需要写纯净的代码
function ProductPage({ productId }) {
  const product = useProduct(productId);
  
  // 直接写,编译器自动判断是否需要缓存
  const recommendations = computeRecommendations(product);
  const handleAdd = () => addToCart(product);
  
  return (
    <div>
      {/* 编译器自动处理组件级别的 memo */}
      <Recommendations items={recommendations} />
      <Button onClick={handleAdd} />
    </div>
  );
}

/**
 * 编译器编译后的内部实现(简化版):
 */
function ProductPage_compiled({ productId }) {
  const $ = useMemoCache(6); // 6个缓存槽位
  
  const product = useProduct(productId);
  
  let recommendations;
  if ($[0] !== product) {
    recommendations = computeRecommendations(product);
    $[0] = product;
    $[1] = recommendations;
  } else {
    recommendations = $[1];
  }
  
  let handleAdd;
  if ($[2] !== product) {
    handleAdd = () => addToCart(product);
    $[2] = product;
    $[3] = handleAdd;
  } else {
    handleAdd = $[3];
  }
  
  let jsx;
  if ($[4] !== recommendations || $[5] !== handleAdd) {
    jsx = (
      <div>
        <Recommendations items={recommendations} />
        <Button onClick={handleAdd} />
      </div>
    );
    $[4] = recommendations;
    $[5] = handleAdd;
  } else {
    jsx = $[5]; // 连 JSX 都被缓存了!
  }
  
  return jsx;
}

十五、完整生命周期总结

┌─────────────────────────────────────────────────────────────┐
│                useMemo 完整生命周期                          │
│                                                             │
│  ┌─────── 首次渲染 (Mount) ───────┐                         │
│  │                                │                         │
│  │  1. renderWithHooks()          │                         │
│  │     dispatcher = OnMount       │                         │
│  │                                │                         │
│  │  2. Component() 执行           │                         │
│  │     ↓                          │                         │
│  │  3. useMemo(create, deps)      │                         │
│  │     ↓                          │                         │
│  │  4. mountMemo()                │                         │
│  │     - mountWorkInProgressHook()│  ← 创建Hook节点          │
│  │     - create() 执行            │  ← 计算值               │
│  │     - hook.memoizedState =     │                         │
│  │       [value, deps]            │  ← 存储缓存             │
│  │     - return value             │                         │
│  │                                │                         │
│  └────────────────────────────────┘                         │
│                    │                                        │
│                    ▼ (用户交互/状态变更触发重渲染)            │
│                                                             │
│  ┌─────── 更新渲染 (Update) ──────┐                         │
│  │                                │                         │
│  │  1. renderWithHooks()          │                         │
│  │     dispatcher = OnUpdate      │                         │
│  │                                │                         │
│  │  2. Component() 执行           │                         │
│  │     ↓                          │                         │
│  │  3. useMemo(create, newDeps)   │                         │
│  │     ↓                          │                         │
│  │  4. updateMemo()               │                         │
│  │     - updateWorkInProgress-    │                         │
│  │       Hook()                   │  ← 从链表取旧Hook        │
│  │     - prevState =              │                         │
│  │       hook.memoizedState       │  ← 读取 [oldVal, oldDeps]│
│  │     ↓                          │                         │
│  │  5. areHookInputsEqual(        │                         │
│  │       newDeps, oldDeps)        │  ← 逐项 Object.is 比较  │
│  │     ↓                          │                         │
│  │  ┌──┴──────────┐              │                         │
│  │  │             │              │                         │
│  │  ▼             ▼              │                         │
│  │ 相等          不相等          │                         │
│  │  │             │              │                         │
│  │  ▼             ▼              │                         │
│  │ return       create()执行     │                         │
│  │ oldValue     newValue         │                         │
│  │              │                │                         │
│  │              ▼                │                         │
│  │           hook.memoized-      │                         │
│  │           State =             │                         │
│  │           [newVal, newDeps]   │                         │
│  │              │                │                         │
│  │              ▼                │                         │
│  │           return newValue     │                         │
│  │                                │                         │
│  └────────────────────────────────┘                         │
│                    │                                        │
│                    ▼ (组件卸载)                              │
│                                                             │
│  ┌─────── 卸载 (Unmount) ─────────┐                         │
│  │                                │                         │
│  │  Fiber 节点被删除              │                         │
│  │  hook 链表随之释放             │                         │
│  │  缓存的值被 GC 回收           │                         │
│  │                                │                         │
│  │  ⚠️ useMemo 没有 cleanup      │                         │
│  │  (与 useEffect 不同)          │                         │
│  │                                │                         │
│  └────────────────────────────────┘                         │
└─────────────────────────────────────────────────────────────┘

十六、设计哲学与核心总结

┌─────────────────────────────────────────────────────────┐
│                 useMemo 设计哲学                         │
│                                                         │
│  1. 最小存储原则                                        │
│     只存 [value, deps],不存 create 函数                │
│     因为 create 每次渲染都是新的闭包                    │
│                                                         │
│  2. 顺序调用契约                                        │
│     依靠链表位置匹配新旧 hook                           │
│     不需要 key/name,最小化运行时开销                   │
│     代价:不能条件调用                                  │
│                                                         │
│  3. 浅比较策略                                          │
│     Object.is 逐项比较,不做深比较                      │
│     深比较成本不可控,违背"可预测性能"的原则            │
│                                                         │
│  4. 无保证缓存                                          │
│     官方文档明确说:React 可能在某些情况下               │
│     丢弃缓存重新计算(如 offscreen 场景)               │
│     useMemo 是性能优化手段,不是语义保证                │
│                                                         │
│  5. 与 Concurrent Mode 的协作                           │
│     render 阶段是纯的、可中断的、可重复的               │
│     useMemo 的 create 函数必须是纯函数                  │
│     这样即使被多次调用也不会出问题                      │
│                                                         │
│  核心一句话:                                           │
│  useMemo 用一个 [value, deps] 数组 + Object.is 浅比较  │
│  实现了"跳过不必要的重复计算"这一优化                   │
└─────────────────────────────────────────────────────────┘