人人都会AI编程

useMemo /useCallback:值与函数缓存

更新时间:2026-07-11

在 React 函数组件中,每次渲染都会重新执行整个函数体——所有变量、计算、函数声明都会重新创建。这在大多数情况下并没有问题,但当某些计算开销较大,或者某些函数引用被用作子组件的 props 导致子组件无谓重渲染时,就需要对函数进行缓存。useMemouseCallback 正是为此而生。

useMemo:缓存计算结果

useMemo 用于缓存一个。它接收一个“计算函数”和一个依赖数组。只有当依赖项发生变化时,它才会重新执行计算函数并返回新值;否则会直接返回之前缓存的值。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

典型场景:

  1. 避免重复的高开销计算

比如对大列表进行排序、过滤、复杂数学运算等。如果这些计算在每次渲染时都执行,即使相关状态未变,也会浪费资源。

   function ProductList({ products, filterKeyword }) {
     // 每次 render 都过滤,即使 keyword 和 products 没变
     const filtered = useMemo(() => {
       return products.filter(p => p.name.includes(filterKeyword));
     }, [products, filterKeyword]);

     return (
       <ul>
         {filtered.map(p => <li key={p.id}>{p.name}</li>)}
       </ul>
     );
   }
   

在上面的例子中,只有当 productsfilterKeyword 真正变化时,才会重新执行过滤,否则直接复用上次的结果。

  1. 作为其他 Hook 的依赖项时保持引用稳定

如果一个派生值会被用在 useEffectuseMemo 的依赖数组中,保持其引用稳定可以避免副作用或后续计算的频繁触发。

useCallback:缓存函数引用

useCallback 用于缓存一个函数。它的签名与 useMemo 相似,只不过缓存的是函数本身。

const memoizedCallback = useCallback(
  () => { doSomething(a, b); },
  [a, b],
);

实际上,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

典型场景:

  1. 将函数传给使用了 React.memo 的子组件

如果子组件通过 React.memo 包装了,那么它会浅比较 props 是否变化。如果父组件在每次渲染时都创建新的函数引用(即使逻辑相同),子组件就会认为 props 发生了变化,从而不必要地重渲染。使用 useCallback 可以保持函数引用不变,配合 React.memo 避免子组件无谓更新。

   const Parent = () => {
     const [count, setCount] = useState(0);

     // 不使用 useCallback:每次 render 都会创建新函数引用
     // <Child onClick={() => setCount(c => c + 1)} />

     // 使用 useCallback:只有当依赖变化时才会生成新函数
     const handleClick = useCallback(() => {
       setCount(c => c + 1);
     }, []); // 空依赖表示函数永远不变

     return <Child onIncrement={handleClick} />;
   };

   const Child = React.memo(({ onIncrement }) => {
     console.log('Child 渲染');
     return <button onClick={onIncrement}>+1</button>;
   });
   
  1. 作为其他 Hook 的依赖项(如 useEffect

如果一个函数在 useEffect 内部被调用,并且你把它写在了依赖数组中,那么稳定的函数引用可以避免副作用不必要的重复执行。

二者关系与选择总结

| 特性 | useMemo | useCallback |
|------|-----------|---------------|
| 缓存目标 | 任意值(对象、数组、原始值) | 函数引用 |
| 本质 | useMemo(() => value, deps) | useMemo(() => fn, deps) |
| 主要用途 | 跳过重计算、保持引用 | 保持函数引用不变,减少子组件渲染 |
| 与 React.memo 协作 | 也可缓存对象/数组 Props | 直接用于缓存回调函数 Props |

重要提醒:不要过早优化

useMemouseCallback 本身也有开销(存储缓存、依赖比较)。对于大多数简单计算和大多数小型函数,直接创建反而更快。滥用它们不仅让代码变复杂,还可能降低性能。

最佳实践:

  • 先写出清晰、正确的代码,不添加任何记忆化。
  • 当通过 React DevTools Profiler 发现确实存在性能瓶颈(如列表渲染卡顿、高频重渲染)时,再针对性地使用 useMemo / useCallback
  • 重点关注传递给子组件的引用类型 props高开销计算,而不是对每一个变量都加缓存。

常见陷阱与避坑

  1. 依赖数组不完整

忘记声明依赖会导致缓存永远不会更新,形成“过期闭包”。请严格遵循 ESLint 的 react-hooks/exhaustive-deps 规则。

  1. 滥用导致代码可读性下降

无脑包裹 useMemo/useCallback 会让代码变得晦涩。先确认问题再动手。

  1. useMemo 替代 useState + useEffect

不要把 useMemo 当作“根据 props 计算 state”的手段,派生状态通常应该直接计算,或通过 useState 配合 useEffect 来控制(视复杂度而定)。

实用示例:合并使用

function UserList({ users, sortOrder }) {
  // 1. 缓存排序后的列表
  const sortedUsers = useMemo(() => {
    return [...users].sort((a, b) => 
      sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
    );
  }, [users, sortOrder]);

  // 2. 缓存删除回调,保持引用不变
  const handleDelete = useCallback((id) => {
    // 调用外部删除方法
    onDeleteUser(id);
  }, [onDeleteUser]); // 假设 onDeleteUser 是外部传入并已稳定的引用

  return (
    <ul>
      {sortedUsers.map(user => (
        <UserItem 
          key={user.id} 
          user={user} 
          onDelete={handleDelete} 
        />
      ))}
    </ul>
  );
}

在这个例子中,sortedUsers 避免了每次渲染都重新排序(假设 users 很大),handleDelete 保持引用不变以便传递给 React.memo 优化的 UserItem

小结

useMemouseCallback 是 React 提供的缓存工具,用于精细化控制值和函数的再生成时机。它们本身并不神奇,而是在明确性能瓶颈后的一剂“优化药”。记住:先让代码正确运行,再在关键路径上做记忆化