useCallback — 是一个 React Hook,它返回函数的记忆化版本,只要依赖项不发生变化,该函数在渲染之间就不会改变。与在组件内部常规声明函数(每次渲染都会创建新函数)不同,useCallback 会稳定函数的引用,从而防止通过 React.memo 优化的子组件发生不必要的重渲染。根据 React Documentation (2025),useCallback 只有与 React.memo 配合使用或与依赖稳定引用的 Hook 搭配时才有效。
要点
useCallback — 是在 React 16.8 中添加的 Hook,用于对函数进行记忆化:在依赖项发生变化之前,它返回相同的引用。如果没有 useCallback,组件内部每次声明函数都会在每次渲染时创建新的函数对象。对于基本类型来说这不可察觉,但当把这类回调传递给通过 React.memo 优化的子组件时,每个新引用都会导致子组件重渲染。
从语法上讲,useCallback 对于函数来说等同于 useMemo:useCallback(fn, deps) 是 useMemo(() => fn, deps) 的简写。React 将记忆化的函数存储在 fiber 节点的内部存储中,并在每次渲染时比较依赖项。如果依赖项没有变化(每个元素使用 Object.is),则返回之前的函数。
根据 React Documentation — useCallback (2025),不必把每个函数都包装到 useCallback 中。该 Hook 有其自身的代价:调用 Hook、比较依赖项以及为依赖数组分配内存。如果组件很简单,没有与 React.memo 配合的深层组件树——useCallback 只会拖慢应用。优化必须是可衡量的,而不是凭直觉。
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 稳定引用——在 deps 变化前始终是同一函数
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
useCallback 中的记忆化基于缓存函数调用的结果。React 保留在第一次渲染时创建的闭包,并在后续渲染中返回它,只要依赖项保持不变。在 fiber 节点内部,每次调用 useCallback 都会在 Hook 的链表中创建一个节点,用于存储先前的依赖项和记忆化的值。
依赖项的比较过程严格通过 Object.is 进行——这是一种浅比较,不深入检查对象或数组。如果依赖项中有对象或数组,每次渲染的新引用都会被当作变化。因此,在依赖数组中需要传递基本类型的值或稳定引用(例如来自 useRef 或 useMemo)。
根据 React Core Team — Optimization Guide (2024),记忆化的成本包括三个部分:每次渲染时分配依赖数组、通过 Object.is 遍历和比较元素,以及在重新创建时垃圾回收的潜在开销。对于包含数百个 useCallback 包装的组件来说,这可能会变得明显——因此,在使用 Hook 时选择性至关重要。
| 场景 | 没有 useCallback | 使用 useCallback |
|---|---|---|
| 创建函数 | 每次渲染都新建 | 依赖稳定时相同 |
| 传递给 React.memo | 子组件会重渲染 | 子组件不会重渲染 |
| 在 useEffect 数组中 | 效果会重新运行 | 效果稳定 |
| 开销 | 最小 | 依赖比较 + 内存 |
有一个广为流传的误区,认为 useCallback 会自动提高性能。实际上,在孤立使用(没有 React.memo)时,useCallback 甚至会因为依赖比较的成本而略微降低应用速度。该 Hook 只有在三种场景中才真正有用:防止 React.memo 组件重渲染、确保 useEffect 中回调的稳定性,以及传递给依赖 reference equality 的自定义 Hook。
规则很简单:在通过 React DevTools Profiler 发现性能问题之前——不要使用 useCallback。React 团队多次强调,过早优化是万恶之源。先编写不使用记忆化的干净代码,进行测量,在分析器里找到瓶颈,然后才在真正需要的地方添加 useCallback。
// 可衡量的优化:Child 被 React.memo 包装
const Child = React.memo(({ onClick }) => {
console.log('Child 被重渲染');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('已点击');
}, []);
return <Child onClick={handleClick} />;
}
根据 Dan Abramov — Before You memo() (2024),在开源项目中,超过 90% 的 useCallback 使用都是多余的。开发者们为了保险起见包装每个函数,却从不衡量效果。替代方案:如果子组件很重,重渲染成本高——React.memo + useCallback 是合理的。如果子组件很轻——重渲染比比较依赖项更便宜。
第一种场景——React.memo。如果子组件被 React.memo 包装并接收回调函数作为 prop,那么没有 useCallback 时,子组件会在父组件每次渲染时被重绘,即使它自己的数据没有变化。useCallback 稳定了引用,React.memo 就能正确跳过重渲染。
第二种场景——useEffect 的依赖中包含回调。如果函数被传入 useEffect 的依赖数组,每个新引用都会重新运行效果。useCallback 确保引用稳定,效果只在真实数据变化时执行,而不是每次渲染。这对于订阅和请求尤其重要。
// useCallback 用于稳定的 useEffect 依赖
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // 稳定引用,永远不会重新创建
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // 效果仅在 props.id 变化时运行
useCallback 和 useMemo 之间的主要区别在于各自记忆化什么。useCallback 记忆化函数:useCallback(fn, deps) 返回 fn(相同或之前的版本)。useMemo 记忆化函数调用的结果:useMemo(() => computeExpensive(a, b), [a, b]) 返回计算出的值,而不是函数。
从技术上讲,useCallback 是 useMemo 之上的语法糖:useCallback(fn, deps) 等同于 useMemo(() => fn, deps)。这种语法仅仅是为了可读性——让开发者清楚地看到被记忆化的正是函数,而不是值。useCallback 和带函数的 useMemo 之间没有性能差异——它们生成相同的代码。
| Hook | 记忆化 | 语法 | 用途 |
|---|---|---|---|
| useCallback | 函数(引用) | useCallback(fn, deps) | 用于子组件的回调 |
| useMemo | 计算结果 | useMemo(() => value, deps) | 昂贵的计算、对象记忆化 |
// 这些是等价的:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
最常见的错误——在子组件上没有 React.memo 的情况下,把所有函数毫无意义地包装到 useCallback 中。如果子组件没有被 React.memo 包装,它仍然会在父组件每次渲染时重渲染,无论回调的引用是否变化。没有 React.memo 的 useCallback——只有成本,没有收益。
// ❌ 无用:child 上没有 React.memo
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // 没有 React.memo,Child 仍然会重渲染
// ❌ Stale closure:缺少依赖
const handle = useCallback(() => {
console.log(count); // count 始终为 0 —— stale closure!
}, []);
// ✅ 正确:包含依赖项
const handle = useCallback(() => {
console.log(count);
}, [count]);
useCallback 中的 stale closure 问题通过在依赖数组中列出所有使用的变量来解决。带有 exhaustive-deps 的 eslint-plugin-react-hooks 会自动检查回调主体中的所有变量是否都在数组中。如果回调使用了在渲染之间不会改变的 setState,可以安全地将其列入 deps——React 保证 setState 的稳定性。
常见问题
不需要。useCallback 只在三种情况下有意义:子组件被 React.memo 包装、函数用于 useEffect 的依赖数组、或函数被传递给依赖 reference equality 的自定义 Hook。在其他情况下,useCallback 只会增加开销而没有任何好处。React 团队建议先不进行优化编写,然后根据性能分析结果再添加。
对于简单的组件——每次渲染的新函数稍微快一些,因为 useCallback 会花费资源去比较依赖和分配数组。对于具有深层 React.memo 组件树的组件——useCallback 胜出,它防止数千个子元素重渲染。要测量和比较,而不是猜测——使用 React DevTools Profiler 进行客观评估。
可以,useCallback 对 async 函数的工作方式与同步函数完全相同。该 Hook 记忆化的是函数本身,结果(Promise)在每次调用时返回。useCallback 内部的异步函数是用于 useEffect 中稳定的数据加载回调的常见模式:const fetchData = useCallback(async (id) => {...}, [])。
使用 React DevTools Profiler——它会显示哪些组件在重渲染以及为什么。要编程检查,可以添加 console.log 或 useWhyDidYouUpdate——一个记录重渲染原因的库。主要原因是:prop 发生了变化(包括回调引用)、state 或上下文发生了变化。如果 useCallback 没有帮助——检查所有依赖是否都正确列出。
即使没有 React.memo,useCallback 也可以与 useMemo 组合用于上下文 value。如果你将包含函数的对象传递给 Context.Provider,就把对象的创建包装在 useMemo 中,把每个函数包装在 useCallback 中。这样当其中一个函数改变时,可以防止上下文所有使用者的重渲染。但在没有 React.memo 的情况下直接把回调传入 props,useCallback 没有任何好处。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。