它解决了什么问题
在 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 包裹,就不会无谓重渲染。
useCallback 与 useMemo 的关系
useCallback 实际上是 useMemo 的一种特化语法糖:
useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
useMemo 缓存的是计算结果(一个值),useCallback 缓存的是函数本身(一个引用)。如果你需要缓存某个函数的计算结果,用 useMemo;如果你需要缓存一个函数引用以避免子组件不必要的重渲染,用 useCallback。
常见误区与最佳实践
误区一:滥用 useCallback 把所有函数都包裹起来
useCallback 本身也有成本:它会存储函数引用,并且在每次渲染时都会检查依赖数组。如果传递给子组件的函数没有用 React.memo 包裹,或者子组件本身一定会重新渲染(比如子组件依赖的 props 其他部分也会变),使用 useCallback 完全是画蛇添足,还会增加代码复杂度。
原则:先写清晰的代码,用 Profiler 定位性能瓶颈后,再用 useCallback 和 React.memo 精确优化。
误区二:依赖数组忘记包含闭包中使用的变量
function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1); // 使用了 count,但依赖数组为空
}, []);
// ...
}
这个 increment 永远只能得到初始的 count = 0,导致每次点击后 setCount 都是 0 + 1 = 1。有两种正确写法:
- 将
count加入依赖:
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
- 使用函数式更新,避免依赖外部
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的依赖更稳定,缓存效果更好。