前言
在 React 性能优化的工具箱里,useCallback 往往与 React.memo 成对出现。它的存在不是为了让函数运行得更快,而是为了保持函数引用的稳定性,从而避免下游组件不必要的重渲染。
一、 核心概念:为什么要缓存函数?
在 React 函数组件中,每次渲染都会重新执行函数体。这意味着在组件内定义的任何匿名函数,在每次渲染时都会生成一个全新的实例(引用地址不同) 。
- 痛点:如果这个函数被作为 Prop 传递给一个被
React.memo包装的子组件,子组件会因为“Props 变了(引用的函数变了)”而被迫重渲染,导致性能损耗。 - 对策:
useCallback会在内存中“锁住”这个函数实例,除非依赖项发生变化。
二、 语法拆解
callbackFun:要缓存的函数fucn:callbackFun函数执行内容[a,b]:依赖项
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
依赖数组的策略
- 不传数组:每次渲染都重新创建,等同于不使用 Hook。
- 空数组
[]:仅在组件首次挂载时创建一次,整个生命周期引用不变。 - 非空数组
[a, b]:只有当a或b的值发生变化时,才会生成新的函数实例。
三、 实战场景
1. 配合 React.memo 防止子组件重渲染
这是 useCallback 最经典的使用场景。
import React, { useState, useCallback, memo } from 'react';
// 子组件:只有 Props 变化才会重渲染
const ChildButton = memo<{ onClick: () => void }>(({ onClick }) => {
console.log("子组件重渲染...");
return <button onClick={onClick}>增加计数</button>;
});
const Home: React.FC = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ✅ 使用 useCallback 保持引用稳定
// 即使 text 改变触发 Home 渲染,handleClick 引用也不变,子组件就不会重渲染
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="输入不影响子组件" />
<ChildButton onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};
export default Home;
2. 作为其他 Hook 的依赖(如 useEffect)
如果一个函数被用作 useEffect 的依赖,不稳定的函数引用会触发副作用无限循环。
const DataFetcher: React.FC<{ query: string }> = ({ query }) => {
// ✅ 保持 fetchData 稳定
const fetchData = useCallback(() => {
console.log("根据查询条件获取数据:", query);
}, [query]);
useEffect(() => {
fetchData();
}, [fetchData]); // 只有当 query 变化导致 fetchData 变化时,才会触发
return <div>数据查询中...</div>;
};
四、 核心警示:闭包陷阱与依赖项
“缓存”是一把双刃剑。如果依赖项声明不当,会导致函数内部产生“过期”的闭包。
1. 错误示范:闭包陷阱
const [count, setCount] = useState(0);
// ❌ 错误:依赖数组为空,函数内部却使用了 count
const increment = useCallback(() => {
setCount(count + 1); // 这里的 count 被永远锁在了 0
}, []);
2. 解决方案
- 方案 A:将变量加入依赖数组
[count](这会导致函数引用在每次 count 变化时都更新)。 - 方案 B:使用 函数式更新(推荐),这样既能拿到最新值,又不需要把变量加进依赖,保持引用稳定。
// ✅ 最佳实践:既保证了引用稳定,又能获取最新状态
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // 依赖数组可以保持为空
五、 总结:何时使用 useCallback?
- 子组件被
memo包裹,且接收该函数作为 Prop。 - 函数被作为其他 Hook 的依赖。
- 不要过度使用:如果只是普通的按钮点击,且没有子组件性能压力,直接定义匿名函数更简洁。缓存本身也是有开销的。