人人都会AI编程

useMemo /useCallback:缓存依赖对比原理

更新时间:2026-07-11

useMemo 和 useCallback 是 React 提供的两个性能优化 Hook,它们的核心机制都是缓存计算结果或函数引用,并在依赖项未变化时直接返回旧值,避免不必要的计算或引用变更。理解它们的原理,关键在于搞清楚三件事:缓存是什么、何时失效、对比逻辑如何

7.3.3.1 缓存的目的

在 React 中,每次组件重新渲染,函数组件会完整执行一次。这意味着:

  • 在组件内定义的变量、函数都会被重新创建。
  • 如果这些值或函数被传递给子组件作为 Props,子组件可能会因为引用变更而触发不必要的重渲染(即使值逻辑上没变)。

useMemo 缓存计算结果,useCallback 缓存函数本身。它们共同的作用是:保持引用稳定,避免因渲染次数增加而导致的不必要计算和子组件重渲染

// 每次渲染都会重新创建 handleClick,子组件 PureButton 会误判 Props 变化
function Parent() {
  const handleClick = () => console.log('clicked');
  return <PureButton onClick={handleClick} />; // PureButton 用了 React.memo
}

7.3.3.2 缓存的生命周期

useMemo 和 useCallback 的缓存不是全局的,它们依附于当前组件的 Fiber 节点,并存在 Hooks 链表中。同一个组件在同一个生命周期内的多次渲染,会复用这条链表,从而可以对比依赖。

  • 首次渲染:创建缓存值/函数,存入 Hook 状态。
  • 后续渲染:取出上一次的依赖数组和缓存值,与本次传入的依赖数组进行浅比较
  • 如果每个依赖项都相同 → 直接返回缓存值。
  • 如果任一依赖项改变 → 重新计算并更新缓存,下次将使用新缓存。

组件卸载时,整个 Hooks 链表随着 Fiber 节点被回收,缓存自然释放。

7.3.3.3 依赖对比的原理:浅比较(Shallow Equality)

React 使用 Object.is 算法对依赖数组中的每一项进行对比,这是一种浅比较,而非深比较。具体行为:

  1. 遍历依赖数组,将本次依赖项 deps[i] 与上次 prevDeps[i] 进行对比。
  2. 比较规则等同于 Object.is(deps[i], prevDeps[i])
  • 基本类型:值相等即相等。
  • 引用类型:必须指向同一个对象才相等,哪怕内部属性相同也会判为不等。
  1. 如果任意一个依赖项不相等,缓存失效,执行新计算。
const obj = { name: 'Alice' };
// 每次渲染 { name: 'Alice' } 都是新对象,即使内容一样
useMemo(() => expensive(obj), [obj]);
// 如果 obj 每次重新创建,缓存实际上每次都失效

为什么不用深比较?
深比较成本高,而且可能掩盖依赖设计问题。如果你的依赖是对象,应该想办法保持它的引用稳定(例如通过 useMemo 包裹对象创建),而不是依赖深比较来救命。

7.3.3.4 useMemo 的实现简版

从原理上,useMemo 可以简化为如下逻辑(并非 React 源码完全一致,但反映核心流程):

let hookState = []; // 模拟组件 Fiber 上的 hooks 链表
let hookIndex = 0;

function useMemo(factory, deps) {
  const currentIndex = hookIndex;
  const prev = hookState[currentIndex]; // 取出上次存储的状态

  if (prev) {
    // 依赖对比
    const [prevResult, prevDeps] = prev;
    let depsChanged = false;
    if (!deps || !prevDeps) {
      depsChanged = true;
    } else {
      for (let i = 0; i < deps.length; i++) {
        if (!Object.is(deps[i], prevDeps[i])) {
          depsChanged = true;
          break;
        }
      }
    }

    if (!depsChanged) {
      // 依赖未变,返回旧结果
      hookIndex++;
      return prevResult;
    }
  }

  // 需要重新计算
  const newResult = factory();
  hookState[currentIndex] = [newResult, deps];
  hookIndex++;
  return newResult;
}

useCallback 的实现几乎一模一样,只是它缓存的是函数本身:

function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

事实上,useCallback(fn, deps) 完全等价于 useMemo(() => fn, deps)

7.3.3.5 依赖项传空数组与不传的区别

  • useMemo(fn, []):只在首次渲染计算,之后永远返回缓存值,更像 componentDidMount 中的一次性计算。
  • useMemo(fn):不传依赖数组,每次渲染都会重新计算,缓存形同虚设(React 实际上会直接跳过缓存对比)。
  • useMemo(fn, undefined) 等同于不传,每次重新计算。

7.3.3.6 实际应用中的常见坑

1. 缓存失效陷阱:依赖了每次渲染都会变化的值

function Component({ items }) {
  // ❌ items 每次渲染可能都是新数组,缓存无效
  const total = useMemo(() => sum(items), [items]);
}

解决办法:确保依赖项引用稳定,或改用其他状态管理确保 items 只在真正变化时改变。

2. 过度使用导致代码臃肿

useMemo 和 useCallback 本身也有开销(依赖对比),对于简单计算或不会传给子组件的函数,不必包裹。没有性能瓶颈时,不要过早优化。

3. 依赖缺失导致闭包陈旧值

function Timer() {
  const [count, setCount] = useState(0);
  const log = useCallback(() => {
    console.log(count); // 闭包可能引用了旧的 count
  }, []); // 空依赖,log 永远不会更新引用的 count
}

正确做法:将 count 加入依赖数组,或使用 useRef 存储最新值。

7.3.3.7 总结

useMemo 和 useCallback 的缓存机制是基于依赖数组的浅比较,保持引用稳定是其核心用途。理解这个对比原理,可以帮助我们:

  • 准确判断何时缓存有效。
  • 避免因为错误的依赖导致缓存失效或闭包过期问题。
  • 在性能优化与代码可读性之间做出合理权衡。

本质上,它们只是利用了 Hooks 链表的记忆能力,配合简单的依赖比对,实现了一种轻量级的惰性求值。