人人都会AI编程

useCallback:缓存函数引用

更新时间:2026-07-10

它解决了什么问题

在 React 中,组件每次渲染都会重新执行函数体内的代码。这意味着如果你在函数组件内部定义了一个普通函数,每次渲染都会创建一个全新的函数实例:

function Child({ onButtonClick }) {
  // Child 每次接收到新的 onButtonClick 时也可能重新渲染
  return <button onClick={onButtonClick}>Click me</button>;
}

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

  // 每次 Parent 渲染,这个 handleClick 都是一个全新的函数
  const handleClick = () => {
    setCount(c => c + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <Child onButtonClick={handleClick} />
    </div>
  );
}

如果 Child 使用了 React.memo 来进行性能优化,那么每当 Parent 重新渲染,handleClick 的引用变化都会导致 Child 也重新渲染,即便 count 没有变(比如父组件因其他原因重渲染)。这就抵消了 React.memo 的优化效果。

useCallback 的作用就是:在依赖不变的情况下,始终返回同一个函数引用。

基本用法

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
  • 第一个参数:要缓存的函数。
  • 第二个参数:依赖数组。只有当数组中的依赖值发生变化时,才会返回新的函数引用;否则返回上一次缓存的函数引用。

实际示例

优化上面的例子:

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

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 依赖为空,这个函数定义不依赖任何外部变量,所以只在组件首次渲染时创建一次

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <p>{count}</p>
      <Child onButtonClick={handleClick} />
    </div>
  );
}

现在,即使 name 变化引发 Parent 重渲染,handleClick 的引用保持不变,Child 如果被 React.memo 包裹,就不会无谓重渲染。

useCallbackuseMemo 的关系

useCallback 实际上是 useMemo 的一种特化语法糖:

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

useMemo 缓存的是计算结果(一个值),useCallback 缓存的是函数本身(一个引用)。如果你需要缓存某个函数的计算结果,用 useMemo;如果你需要缓存一个函数引用以避免子组件不必要的重渲染,用 useCallback

常见误区与最佳实践

误区一:滥用 useCallback 把所有函数都包裹起来

useCallback 本身也有成本:它会存储函数引用,并且在每次渲染时都会检查依赖数组。如果传递给子组件的函数没有用 React.memo 包裹,或者子组件本身一定会重新渲染(比如子组件依赖的 props 其他部分也会变),使用 useCallback 完全是画蛇添足,还会增加代码复杂度。

原则:先写清晰的代码,用 Profiler 定位性能瓶颈后,再用 useCallbackReact.memo 精确优化。

误区二:依赖数组忘记包含闭包中使用的变量

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

  const increment = useCallback(() => {
    setCount(count + 1); // 使用了 count,但依赖数组为空
  }, []);
  // ...
}

这个 increment 永远只能得到初始的 count = 0,导致每次点击后 setCount 都是 0 + 1 = 1。有两种正确写法:

  1. count 加入依赖:
   const increment = useCallback(() => {
     setCount(count + 1);
   }, [count]);
   
  1. 使用函数式更新,避免依赖外部 count
   const increment = useCallback(() => {
     setCount(c => c + 1);
   }, []);
   

第二种更优,因为依赖数组为空,函数引用永远不变。

useCallback 在自定义 Hooks 中的常见场景

当你封装自定义 Hook 并返回一个函数时,常常需要 useCallback 来稳定引用,否则使用该 Hook 的组件可能会拿到不断变化的函数,导致意料之外的重渲染或效应重复执行:

function useToggle(initial = false) {
  const [state, setState] = useState(initial);
  const toggle = useCallback(() => {
    setState(s => !s);
  }, []); // 函数式更新,依赖为空

  return [state, toggle];
}

这样,任何使用 useToggle 的组件获取的 toggle 引用都是稳定的,可以放心传递给子组件或作为 useEffect 的依赖。

总结

  • 什么时候用:当你把函数作为 prop 传递给使用 React.memo 的子组件,且该函数依赖的状态很少变化时。
  • 什么时候不用:函数只在当前组件内部使用(不作为 prop 或 effect 依赖)、子组件没有性能化包裹,或者依赖频繁变化导致缓存毫无意义。
  • 配合函数式更新:可以利用函数式 setState 避免将状态加入依赖数组,从而让 useCallback 的依赖更稳定,缓存效果更好。