useCallback 是 React 中一个重要的性能优化 Hook,用于缓存函数引用 ,避免在组件重新渲染时创建新的函数实例。它在配合 React.memo、子组件优化、事件处理函数传递等场景中非常有用。
一、基本原理
1. 函数是引用类型
在 JavaScript 中,每次定义函数(即使是内容完全相同的函数),都会创建一个新的引用:
ini
js
编辑
const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false
在 React 函数组件中,每次渲染都会重新执行整个函数体,因此其中定义的函数也会被重新创建。
2. 问题:不必要的子组件重渲染
当父组件将内联函数作为 prop 传给子组件(尤其是用 React.memo 包裹的子组件)时,由于函数引用变化,即使 props 内容没变,子组件也会重新渲染。
3. 解决方案:useCallback
useCallback 通过依赖数组(deps)缓存函数引用,只有在依赖项变化时才返回新函数,否则返回上一次缓存的函数。
ini
js
编辑
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
其内部实现逻辑大致如下(简化版):
ini
js
编辑
function useCallback(callback, deps) {
const hook = getHook(); // 获取当前 hook 状态
if (depsChanged(hook.deps, deps)) {
hook.callback = callback;
hook.deps = deps;
}
return hook.callback;
}
注意:
useCallback(fn, deps)等价于useMemo(() => fn, deps)。
二、详细用法
1. 基本用法:缓存事件处理函数
javascript
jsx
编辑
import { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 缓存 handleClick,仅当 count 变化时才更新
const handleClick = useCallback(() => {
console.log('Clicked!', count);
}, [count]);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={() => setCount(c => c + 1)}>+</button>
{/* 传递缓存后的函数 */}
<Child onClick={handleClick} />
</div>
);
}
// 使用 React.memo 避免无意义重渲染
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
如果没有
useCallback,每次输入 name 都会导致handleClick引用变化,从而触发Child重渲染。
2. 与自定义 Hook 结合使用
ini
js
编辑
function useApiCall(url) {
const fetch = useCallback(async () => {
const res = await fetch(url);
return res.json();
}, [url]);
return fetch;
}
确保在 url 不变时,fetch 函数引用不变,便于其他组件依赖它。
3. 用于高阶函数或回调链
scss
js
编辑
const handleSave = useCallback((data) => {
api.save(data).then(onSuccess);
}, [onSuccess]); // onSuccess 本身也应是稳定引用
注意:如果 onSuccess 是父组件传入的函数,也建议用 useCallback 包裹,否则会导致 handleSave 频繁变化。
4. 与 useEffect 配合(作为依赖)
scss
js
编辑
const fetchData = useCallback(() => {
// ...
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // 安全地作为依赖
如果不使用 useCallback,fetchData 每次渲染都不同,会导致 useEffect 无限循环或频繁执行。
三、注意事项(重要!)
✅ 1. 不要滥用
useCallback本身有轻微性能开销(比较依赖、存储缓存)。- 如果函数不作为 prop 传给
React.memo子组件,或不在useEffect/useMemo中作为依赖,通常不需要 用useCallback。 - 过度使用会增加代码复杂度,得不偿失。
经验法则 :只有当函数被用作依赖项 或传递给优化过的子组件 时,才考虑
useCallback。
✅ 2. 依赖数组必须完整且正确
- 必须包含函数体内用到的所有响应式值(state、props、其他 hooks 返回值等)。
- 否则可能导致闭包陷阱(stale closure)------函数使用的是旧值。
❌ 错误示例:
scss
js
编辑
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log(count); // 依赖 count
}, []); // ❌ 缺少依赖
✅ 正确:
ini
js
编辑
const handleClick = useCallback(() => {
console.log(count);
}, [count]); // ✅
✅ 3. 函数参数不影响缓存
useCallback 缓存的是函数引用,不是调用结果。即使你传不同参数调用,函数本身仍是同一个引用(只要依赖没变)。
✅ 4. 箭头函数 vs 命名函数
两种写法都可以,但要注意依赖:
javascript
js
编辑
// 写法1:箭头函数
const fn = useCallback((x) => x + a, [a]);
// 写法2:命名函数
const fn = useCallback(function add(x) {
return x + a;
}, [a]);
效果相同。
✅ 5. 与 useRef 的区别
useRef可以保存可变值,但不会触发重渲染。useCallback是为了保持函数引用稳定,常用于优化。- 有时可用
useRef保存函数来绕过依赖问题,但会失去响应性,一般不推荐。
✅ 6. 开发模式下可能"失效"
React 在 Strict Mode 下会故意双调用某些函数(包括 useCallback 的回调),这是为了帮助发现副作用,不影响生产行为。
四、常见误区
| 误区 | 说明 |
|---|---|
| "所有函数都要用 useCallback" | ❌ 只有需要稳定引用时才用 |
| "useCallback 能提升函数执行速度" | ❌ 它只缓存引用,不影响执行性能 |
| "空依赖数组总是安全的" | ❌ 如果函数用了外部变量,必须加入依赖 |
五、替代方案
- 如果子组件很小,不值得用
React.memo+useCallback,直接让其重渲染更简单。 - 对于复杂状态逻辑,考虑用
useReducer,dispatch 函数是稳定的,无需useCallback。 - 使用
useEvent(实验性 Hook,未来可能加入 React)可自动处理稳定回调,无需手动管理依赖。
总结
useCallback 的核心价值是:在需要函数引用稳定的场景下,避免不必要的重渲染或重复订阅。
✅ 正确使用场景:
- 传递给
React.memo子组件的函数 props - 作为
useEffect、useMemo、useCallback的依赖 - 传递给第三方库(如事件监听器、动画回调等)需稳定引用的函数
🚫 避免:
- 为"看起来高级"而加
- 忽略依赖项
- 在简单组件中过度优化
合理使用 useCallback,能让你的 React 应用既高效又可维护。