人人都会AI编程

useMemo 与 useCallback 的适用场景与误区

更新时间:2026-07-10

useMemouseCallback 是 React 提供的两个用于性能优化的 Hook。它们都利用缓存(memoization)机制来避免不必要的重新计算或重新创建,但在实际开发中,它们经常被误用,甚至反而降低了代码质量和可维护性。理解它们的正确使用场景和常见误区,是写出高效 React 代码的关键。

useMemo:缓存昂贵的计算结果

useMemo 接受一个“创建函数”和一个依赖数组,只有当依赖项发生变化时,才会重新执行创建函数,否则直接返回上次缓存的值。

基本语法:

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

适用场景:

  1. 真正的计算密集型任务

对大量数据进行排序、过滤、复杂转换,且该计算并不需要在每次渲染时都执行。

   function ProductList({ products, filterText }) {
     // 只有当 products 或 filterText 改变时才重新筛选排序
     const filtered = useMemo(() => {
       return products
         .filter(p => p.name.includes(filterText))
         .sort((a, b) => b.price - a.price);
     }, [products, filterText]);
     
     return <List data={filtered} />;
   }
   
  1. 引用稳定性(避免子组件不必要的重渲染)

当把一个对象或数组作为 Props 传递给使用 React.memo 包裹的子组件时,如果该引用在每次渲染时都重新创建,会导致子组件即使内容相同也会被重新渲染。

   const styles = useMemo(() => ({ color: 'red', fontWeight: 'bold' }), []);
   <ExpensiveChild style={styles} />;
   

useMemo 保证了在依赖不变的情况下返回同一个引用,从而配合 React.memo 的浅比较跳过子组件渲染。

常见误区:

  • 过早优化

大多数情况下,JavaScript 的计算速度远快于 DOM 操作,简单的加法、字符串拼接根本无需缓存。加上 useMemo 本身也需要进行依赖比较和内存占用,可能得不偿失。
❌ 错误示例:

  const total = useMemo(() => price + tax, [price, tax]);
  
  • 依赖数组遗漏或不完整

如果依赖数组遗漏了内部使用的变量,会导致缓存失效或始终使用旧值,引发难以排查的 bug。ESLint 的 react-hooks/exhaustive-deps 规则能帮助检查,务必遵守。

  • 把 useMemo 当成语义保证

React 文档明确说明:useMemo 只作为性能优化手段,React 可能在将来为了释放内存而丢弃缓存值(尽管当前实现不会)。因此不能依赖它的缓存行为来保证逻辑正确性。

useCallback:缓存函数引用

useCallback 本质上是对 useMemo 的语法糖,专用于缓存函数引用。如果传给子组件的函数每次渲染都重新创建,同样会破坏 React.memo 的优化效果。

基本语法:

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

适用场景:

  1. 传递给 React.memo 子组件的回调函数

当父组件状态变化时,如果不缓存传递给子组件的函数,子组件哪怕其他数据没变,也会因为函数引用不同而重新渲染。

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

     const increment = useCallback(() => {
       setCount(c => c + 1);
     }, []); // 空依赖,该函数永远不变

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

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

如果你在 useEffect 中调用了某个函数,并把它列入了依赖数组,那么该函数需要使用 useCallback 缓存,否则每次渲染都会触发副作用重新执行。

   const fetchData = useCallback(async () => {
     const res = await api.getData(id);
     // ...
   }, [id]);

   useEffect(() => {
     fetchData();
   }, [fetchData]);
   

常见误区:

  • 无差别包裹所有函数

许多新手看到子组件就用 useCallback 包裹所有回调,但只有当子组件被 React.memo 包裹时才有意义(或使用了浅比较的其他优化)。普通子组件每次都会重新渲染,缓存反而增加了无用的成本。

  • 忽略闭包陷阱

useCallback 依赖数组中的值发生变化时,新创建的函数会捕获最新的 state;但如果依赖数组不变,函数中引用的 state 就会是旧的。必须确保依赖项完整,否则会出现“读不到最新值”的问题。

  const [text, setText] = useState('');
  const log = useCallback(() => {
    console.log(text); // 如果 text 没有在依赖中,这里永远打印初始值
  }, []); // ❌ 遗漏依赖
  
  • 滥用导致依赖链复杂

为了做到依赖完整,开发者常常在 useCallback 内部使用很多 state 和 props,导致依赖数组变得臃肿,进而产生连锁的 useCallbackuseMemo,使代码难以维护。有时直接使用函数式更新或重构组件结构更为简洁。

useMemo 与 useCallback 的本质与取舍

  • 本质相同useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  • 何时用哪个:如果返回值是普通数据(对象、数组、数字等),用 useMemo;如果返回值本身就是一个函数,且需要保持引用稳定,用 useCallback
  • 先写清洁代码,再优化

在性能问题实际出现之前,不必急于使用它们。大多数应用在中等复杂度的交互下,React 的默认更新已经很快。当通过 React DevTools Profiler 定位到具体的不必要的重渲染时,再有针对性地引入缓存。

最佳实践速查

| 场景 | 推荐做法 |
|------|----------|
| 计算量大且依赖明确的数据转换 | 使用 useMemo |
| 将对象/数组作为 Props 传给 React.memo 子组件 | 使用 useMemo 保持引用稳定 |
| 将回调函数传给 React.memo 子组件 | 使用 useCallback |
| 函数被用在 useEffect / 其他 Hook 的依赖数组里 | 使用 useCallback |
| 纯视图组件无重渲染问题 | 无需使用两者 |
| 简单运算或零成本操作 | 直接计算,不要用缓存 |

总结

useMemouseCallback 是精准的性能手术刀,而不是日常的保健品。在没有必要的地方使用它们,不仅增加了代码复杂度,还可能因为额外的依赖管理引发 bug。记住一条简单原则:只有当确实存在可测量的性能问题时,才打开这些缓存工具;同时务必通过 ESLint 规则保证依赖数组的正确性。