useCallback 是 React 提供的性能优化 Hook,核心作用是缓存函数,避免组件重新渲染时函数被重复创建,主要用于解决子组件不必要的重新渲染问题,是 React 性能优化的常用手段之一。
一、核心作用
- 缓存函数实例:组件重新渲染时,返回缓存的函数引用,而非创建新函数。
- 减少子组件重渲染:当父组件传递函数给子组件时,若函数引用不变,子组件可跳过不必要的重渲染(需配合
memo使用)。 - 优化依赖项:在
useEffect、useMemo等依赖数组中使用,避免因函数引用变化导致依赖项频繁触发。
二、基本用法
1. 引入与语法
从 react 中导入 useCallback,在函数组件顶层调用,语法如下:
import { useCallback } from 'react';
function 组件名() {
// 语法:const 缓存函数 = useCallback(原函数, 依赖数组);
const cachedFn = useCallback(() => {
// 函数逻辑(可访问组件的 props、state 等)
}, [依赖项1, 依赖项2]); // 依赖数组:与 useEffect 一致
return UI结构;
}
2. 关键参数说明
- 原函数:需要缓存的函数,逻辑与普通函数一致。
- 依赖数组:控制缓存的核心,与
useEffect规则完全相同:- 不传依赖数组:组件每次渲染都会返回新函数(等于没缓存,不推荐)。
- 空数组
[]:仅首次渲染创建函数,后续永远返回缓存的函数引用。 - 包含依赖项:仅当依赖项变化时,才会重新创建并缓存新函数,否则返回旧引用。
- 返回值:缓存后的函数实例,依赖不变时,多次渲染返回的函数引用完全相同。
三、实用场景与示例
场景 1:配合 memo 避免子组件重渲染(最常用)
父组件传递函数给子组件时,默认每次父组件渲染都会创建新函数,导致子组件即使 props 未变也会重渲染。用 useCallback 缓存函数,配合 memo 包裹子组件,可解决此问题。
示例代码:
import { useState, useCallback, memo } from 'react';
// 子组件:用 memo 包裹,仅当 props 变化时才重渲染
const Child = memo(({ onClick }) => {
console.log('子组件重渲染'); // 仅用于测试是否重渲染
return <button onClick={onClick}>点击我</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 用 useCallback 缓存函数,依赖数组为空(函数逻辑不依赖任何状态)
const handleClick = useCallback(() => {
alert('子组件按钮被点击');
}, []); // 空依赖:函数引用永远不变
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加计数</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入文本"
/>
{/* 传递缓存后的函数给子组件 */}
<Child onClick={handleClick} />
</div>
);
}
效果说明:
- 点击“增加计数”或输入文本时,父组件重渲染,但
handleClick引用不变。 - 子组件因
memo检测到onClick引用未变,不会重渲染(控制台无“子组件重渲染”日志)。
场景 2:优化 useEffect 依赖项
当 useEffect 依赖一个函数时,若函数每次渲染都变化,会导致 useEffect 频繁执行。用 useCallback 缓存函数,可稳定依赖项。
示例代码:
import { useState, useEffect, useCallback } from 'react';
function DataFetcher() {
const [userId, setUserId] = useState(1);
const [data, setData] = useState(null);
// 缓存请求函数,依赖 userId
const fetchData = useCallback(async () => {
const res = await fetch(`https://api.example.com/user/${userId}`);
const userData = await res.json();
setData(userData);
}, [userId]); // 仅 userId 变化时,重新创建 fetchData
// useEffect 依赖 fetchData,因 fetchData 被缓存,仅 userId 变化时执行
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
<button onClick={() => setUserId(userId + 1)}>切换用户 ID:{userId}</button>
{data && <p>用户名:{data.name}</p>}
</div>
);
}
场景 3:缓存复杂计算函数
若函数内部有复杂计算逻辑,且不频繁变化,用 useCallback 缓存可避免重复计算(配合 useMemo 效果更佳)。
四、核心特性
- 缓存函数引用:本质是缓存函数的内存地址,而非函数逻辑,依赖不变时引用永远相同。
- 必须配合
memo/useMemo生效:单独使用useCallback无性能优化效果,需子组件用memo或其他缓存机制配合。 - 依赖规则严格:与
useEffect一致,函数内使用的外部变量必须加入依赖数组,否则会导致逻辑错误。 - 仅用于性能优化:非必要场景(如简单组件、无频繁重渲染)无需使用,过度使用会增加代码复杂度。
五、常见误区
- 单独使用
useCallback:以为缓存函数就能优化性能,却未用memo包裹子组件,子组件仍会重渲染,优化失效。 - 遗漏依赖项:函数内使用了状态/属性,却未加入依赖数组,导致函数内拿到的是旧值,逻辑错误。
- 过度使用:所有函数都用
useCallback缓存,反而增加代码维护成本,且useCallback本身有微小性能开销,简单函数无需缓存。 - 依赖数组传空却使用外部变量:空依赖数组表示函数不依赖任何变量,若函数内使用了外部状态,需加入依赖,否则会拿到旧值。
六、与 useMemo 的区别
很多人会混淆 useCallback 和 useMemo,核心区别如下:
useCallback:缓存函数本身,返回函数引用。useMemo:缓存函数的执行结果,返回计算后的值。
示例对比:
// useCallback:缓存函数
const fn = useCallback(() => count * 2, [count]);
// useMemo:缓存函数结果
const value = useMemo(() => count * 2, [count]);
总结
useCallback是缓存函数的性能优化 Hook,核心是稳定函数引用,避免子组件不必要重渲染。- 必须配合
memo包裹子组件才能生效,依赖数组规则与useEffect完全一致。 - 仅在需要优化频繁重渲染的场景使用,避免过度优化,简单场景直接用普通函数即可。
