useCallback
缓存函数引用的利器 - 优化子组件渲染性能
核心概述
在 React 组件中,每次重新渲染都会导致组件内的所有代码重新执行, 包括事件处理函数的重新创建。在 JavaScript 中,函数是对象,每次创建都会得到新的引用。 当这些函数作为 props 传递给经过 React.memo 优化的子组件时, 新的函数引用会导致子组件即使 props 值没变也会重新渲染,破坏性能优化。
useCallback 通过缓存函数引用来解决这个问题。 它返回一个在组件多次渲染间保持稳定的函数,只有当依赖数组中的值变化时才创建新函数。 这使得传递给子组件的函数引用保持稳定,让 React.memo 的浅比较优化生效。
核心机制:useCallback 本质上是 useMemo 的语法糖, 专门用于缓存函数。它在首次渲染时创建并缓存函数,后续渲染时检查依赖数组。 如果依赖未变,返回缓存的函数引用;如果依赖变了,创建新函数并更新缓存。
适用场景:当函数作为 props 传递给优化的子组件、 作为其他 Hook(如 useEffect)的依赖、或作为 useMemo 的计算函数时使用 useCallback。 但不要过度优化——只有性能测试表明有必要时才使用。
💡 心智模型
将 useCallback 想象成"函数名片盒":
- • 首次名片:创建函数并打印名片(引用),存入名片盒
- • 后续使用:有人要名片时,先检查盒子里是否还有名片
- • 检查内容:对比名片上的"依赖内容"是否变化
- • 复用 vs 重印:内容没变就给旧名片,内容变了才重新打印
关键:名片上的"依赖内容"指的是函数内部使用的外部变量(如 props、state)。 只有这些变量变化时,才需要重新创建函数。
技术规格
类型签名
function useCallback<T extends (...args: any[]) => any>(
callback: T,
dependencies: DependencyList
): T
// 泛型 T 是被缓存函数的类型
// callback 是要缓存的函数
// dependencies 是依赖数组,只有数组中的值变化时才重新创建函数参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
callback | T | 要缓存的函数。这个函数可以接受任何参数并返回任何值。 React 会在首次渲染时返回这个函数本身。 |
dependencies | DependencyList | 依赖数组。React 使用 Object.is 比较数组中每个值与上次渲染的值。 只要有一个值变化,就会重新创建并缓存新函数。 |
返回值
| 返回值 | 类型 | 说明 |
|---|---|---|
| 缓存函数 | T | 首次渲染返回 callback 函数本身。后续渲染中,如果依赖未变, 返回上次缓存的函数引用(相同的内存地址);如果依赖变了,返回新创建的函数。 |
运行机制
等价实现:useCallback 实际上是 useMemo 的语法糖:
// 这两个完全等价:
const cachedFn = useCallback(() => doSomething(a, b), [a, b]);
const cachedFn = useMemo(() => () => doSomething(a, b), [a, b]);引用稳定性:缓存函数在依赖不变时保持同一个引用(内存地址)。 这对于 React.memo 的浅比较很重要——如果对象/函数引用相同,React 认为值没变。
性能权衡:useCallback 本身也有开销——依赖比较和存储缓存。 不要对所有函数都使用,只在确实需要稳定引用的场景使用。
实战演练
1. 基础用法:优化子组件渲染
最常见的场景是配合 React.memo 优化子组件渲染:
import { useCallback, useState, memo } from 'react';
interface PostProps {
id: number;
title: string;
onLike: (id: number) => void;
}
// ✅ 子组件使用 React.memo 优化
const Post = memo(function Post({ id, title, onLike }: PostProps) {
console.log(`Post ${id} 渲染`); // 观察渲染次数
return (
<div className="p-4 border rounded mb-2">
<h3 className="font-semibold">{title}</h3>
<button
onClick={() => onLike(id)}
className="px-3 py-1 bg-blue-500 text-white rounded"
>
点赞
</button>
</div>
);
});
export function PostList({ posts }: { posts: Array<{ id: number; title: string }> }) {
const [likeCount, setLikeCount] = useState(0);
// ✅ 使用 useCallback 稳定函数引用
const handleLike = useCallback((id: number) => {
setLikeCount(c => c + 1);
}, []); // 空依赖,函数永不改变
return (
<div className="p-6">
<p className="mb-4">总点赞数: {likeCount}</p>
{/* 这个按钮会导致 PostList 重新渲染,但 Post 不会重新渲染 */}
<button
onClick={() => setLikeCount(likeCount + 1)}
className="px-4 py-2 mb-4 bg-gray-200 rounded"
>
计数: {likeCount}
</button>
<div>
{posts.map(post => (
<Post
key={post.id}
{...post}
onLike={handleLike} // ✅ 稳定引用,Post 不会重新渲染
/>
))}
</div>
</div>
);
}2. 生产级案例:事件处理器优化
在列表渲染中,使用 useCallback 避免为每个元素创建新的事件处理器:
import { useCallback, useState } from 'react';
interface User {
id: string;
name: string;
email: string;
}
interface UserListProps {
users: User[];
onSelect: (userId: string) => void;
}
export function UserList({ users, onSelect }: UserListProps) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// ✅ 缓存选择函数,避免每次渲染都创建新函数
const handleSelect = useCallback((userId: string) => {
setSelectedId(userId);
onSelect(userId); // 调用父组件传入的回调
}, [onSelect]); // onSelect 依赖变化时才重新创建
return (
<div className="p-6">
<ul className="space-y-2">
{users.map(user => (
<li
key={user.id}
onClick={() => handleSelect(user.id)}
className={`p-3 border rounded cursor-pointer ${
selectedId === user.id
? 'bg-blue-100 border-blue-500'
: 'bg-white hover:bg-gray-50'
}`}
>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-gray-600">{user.email}</p>
</li>
))}
</ul>
</div>
);
}3. 生产级案例:作为 useEffect 依赖
当函数作为 useEffect 的依赖时,使用 useCallback 确保依赖稳定:
import { useCallback, useEffect, useState } from 'react';
interface ChatMessage {
id: string;
content: string;
timestamp: number;
}
export function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isConnected, setIsConnected] = useState(false);
// ✅ 使用 useCallback 缓存消息处理函数
const handleMessage = useCallback((message: ChatMessage) => {
setMessages(prev => [...prev, message]);
}, []);
// ✅ 使用 useCallback 缓存连接状态更新函数
const handleConnectionChange = useCallback((connected: boolean) => {
setIsConnected(connected);
}, []);
// ✅ handleMessage 是稳定依赖,effect 不会频繁重新建立
useEffect(() => {
console.log('建立 WebSocket 连接...');
// 模拟 WebSocket 连接
const ws = {
send: (data: any) => console.log('发送:', data),
close: () => console.log('关闭连接'),
onmessage: (handler: (msg: any) => void) => {
// 模拟接收消息
handler({ content: '新消息', id: '1', timestamp: Date.now() });
},
onopen: () => handleConnectionChange(true),
onclose: () => handleConnectionChange(false),
};
ws.onmessage(handleMessage);
return () => {
ws.close();
};
}, [roomId, handleMessage, handleConnectionChange]);
return (
<div className="p-6">
<div className="mb-4">
状态: {isConnected ? '已连接' : '未连接'}
</div>
<ul className="space-y-2">
{messages.map(msg => (
<li key={msg.id} className="p-2 bg-gray-100 rounded">
{msg.content}
</li>
))}
</ul>
</div>
);
}4. 生产级案例:减少依赖项
使用函数式更新减少依赖,或使用 useReducer 完全消除依赖:
import { useCallback, useState, useReducer } from 'react';
// ❌ 问题:依赖数组很长
function FormWithoutReducer({ user, preferences }: {
user: { id: string; name: string };
preferences: { theme: string; language: string };
}) {
const [status, setStatus] = useState('idle');
const handleSubmit = useCallback(() => {
// 使用多个外部变量
console.log('提交用户:', user.id, user.name);
console.log('主题:', preferences.theme);
console.log('语言:', preferences.language);
setStatus('submitting');
// 提交逻辑...
}, [user, preferences, status]); // ❌ 很多依赖,容易变化
return <button onClick={handleSubmit}>提交</button>;
}
// ✅ 方案 1: 使用函数式更新减少依赖
function FormWithFunctionalUpdate({ userId }: { userId: string }) {
const [status, setStatus] = useState('idle');
const handleSubmit = useCallback(() => {
// ✅ 使用函数式更新,不依赖外部状态
setStatus(prev => {
console.log('提交用户:', userId);
console.log('当前状态:', prev);
return 'submitting';
});
}, [userId]); // ✅ 只依赖 userId
return <button onClick={handleSubmit}>提交</button>;
}
// ✅ 方案 2: 使用 useReducer 消除依赖
type FormState = { status: 'idle' | 'submitting' | 'success' | 'error' };
type FormAction = { type: 'SUBMIT' } | { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SUBMIT':
return { status: 'submitting' };
case 'RESET':
return { status: 'idle' };
default:
return state;
}
}
function FormWithReducer({ user, preferences }: {
user: { id: string; name: string };
preferences: { theme: string; language: string };
}) {
const [state, dispatch] = useReducer(formReducer, { status: 'idle' });
// ✅ dispatch 引用永远稳定,无依赖!
const handleSubmit = useCallback(() => {
console.log('提交用户:', user.id);
console.log('主题:', preferences.theme);
dispatch({ type: 'SUBMIT' });
}, [user, preferences]); // 仍然需要 user 和 preferences
// 更好的方案:将 user 和 preferences 放入 reducer state
return <button onClick={handleSubmit}>提交</button>;
}避坑指南
陷阱 1: 过度使用 useCallback
问题:对所有函数都使用 useCallback,包括只在本地使用的函数。 这会增加代码复杂度,还可能降低性能(依赖比较也有成本)。
// ❌ 过度优化:本地使用的函数不需要 useCallback
function Form() {
const [value, setValue] = useState('');
// ❌ 不必要的 useCallback
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}, []);
const handleSubmit = useCallback(() => {
console.log(value);
}, [value]);
return (
<form>
<input value={value} onChange={handleChange} />
<button onClick={handleSubmit}>提交</button>
</form>
);
}// ✅ 正确:直接声明函数
function Form() {
const [value, setValue] = useState('');
// ✅ 直接声明,简洁清晰
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = () => {
console.log(value);
};
return (
<form>
<input value={value} onChange={handleChange} />
<button onClick={handleSubmit}>提交</button>
</form>
);
}经验法则:只在函数传递给 memo 优化的子组件, 或作为 useEffect/useMemo 依赖时才使用 useCallback。
陷阱 2: 函数使用过期的 props 或 state
问题:依赖数组不完整或为空,导致函数内部使用的是旧闭包值。
// ❌ 错误:空依赖导致闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
// ❌ count 永远是初始值 0!
console.log('当前计数:', count);
setCount(count + 1);
}, []); // 空依赖
return <button onClick={handleClick}>增加</button>;
}// ✅ 方案 1: 添加依赖
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('当前计数:', count);
setCount(count + 1);
}, [count]); // ✅ 添加 count 依赖
return <button onClick={handleClick}>增加</button>;
}
// ✅ 方案 2: 使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
// ✅ 函数式更新,读取最新值
setCount(c => {
const newValue = c + 1;
console.log('新计数:', newValue);
return newValue;
});
}, []); // ✅ 无依赖,不会闭包陷阱
return <button onClick={handleClick}>增加</button>;
}陷阱 3: 依赖包含频繁变化的对象/数组
问题:依赖数组中的对象或数组每次渲染都是新引用, 导致 useCallback 失去缓存效果。
// ❌ 错误:options 对象每次渲染都是新引用
function Component({ items }: { items: number[] }) {
const [filter, setFilter] = useState({ min: 0, max: 100 });
const handleProcess = useCallback(() => {
console.log('最小值:', filter.min);
console.log('最大值:', filter.max);
console.log('处理项目:', items);
}, [filter, items]); // ❌ filter 和 items 每次都是新引用!
return <button onClick={handleProcess}>处理</button>;
}// ✅ 方案 1: 只依赖需要的值
function Component({ items }: { items: number[] }) {
const [filter, setFilter] = useState({ min: 0, max: 100 });
const handleProcess = useCallback(() => {
console.log('最小值:', filter.min);
console.log('最大值:', filter.max);
// ❌ 仍然依赖 items,如果 items 变化还是会重新创建
}, [filter.min, filter.max]); // ✅ 只依赖基本类型
return <button onClick={handleProcess}>处理</button>;
}
// ✅ 方案 2: 使用 useMemo 稳定对象引用
import { useMemo } from 'react';
function Component({ items }: { items: number[] }) {
const [filter, setFilter] = useState({ min: 0, max: 100 });
// ✅ 使用 useMemo 稳定 filter 对象
const stableFilter = useMemo(() => filter, [JSON.stringify(filter)]);
const handleProcess = useCallback(() => {
console.log('过滤器:', stableFilter);
console.log('处理项目:', items);
}, [stableFilter, items]);
return <button onClick={handleProcess}>处理</button>;
}
// ✅ 方案 3: 将 items 作为参数传入
function Component() {
const [items, setItems] = useState<number[]>([]);
// ✅ items 作为参数,不在依赖数组中
const handleProcess = useCallback((itemsToProcess: number[]) => {
console.log('处理项目:', itemsToProcess);
}, []); // ✅ 无外部依赖
return <button onClick={() => handleProcess(items)}>处理</button>;
}陷阱 4: 在 JSX 中创建内联函数
问题:在 JSX 中使用箭头函数或 bind 创建新函数, 即使子组件使用了 useCallback,也会破坏优化。
// ❌ 错误:在 JSX 中创建新函数
function UserList({ users, onSelect }: {
users: Array<{ id: string; name: string }>;
onSelect: (id: string) => void;
}) {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<ul>
{users.map(user => (
<li
key={user.id}
// ❌ 每次渲染都创建新函数!
onClick={() => onSelect(user.id)}
// ❌ 或者这样
onClick={handleSelect.bind(null, user.id)}
// ❌ 或者这样
onClick={() => {
setSelectedId(user.id);
onSelect(user.id);
}}
className={selectedId === user.id ? 'bg-blue-100' : ''}
>
{user.name}
</li>
))}
</ul>
);
}// ✅ 正确:使用 useCallback + 数据属性
import { useCallback } from 'react';
function UserList({ users, onSelect }: {
users: Array<{ id: string; name: string }>;
onSelect: (id: string) => void;
}) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// ✅ 缓存处理函数
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
onSelect(id);
}, [onSelect]);
// ✅ 方案 1: 传递 ID,让子组件处理点击
return (
<ul>
{users.map(user => (
<li
key={user.id}
// ❌ 还是创建了新函数包装器!
// onClick={() => handleSelect(user.id)}
// ✅ 使用 data 属性
data-id={user.id}
onClick={(e) => handleSelect(e.currentTarget.dataset.id || '')}
className={selectedId === user.id ? 'bg-blue-100' : ''}
>
{user.name}
</li>
))}
</ul>
);
}
// ✅ 方案 2: 使用子组件
function UserItem({
user,
onSelect,
isSelected
}: {
user: { id: string; name: string };
onSelect: (id: string) => void;
isSelected: boolean;
}) {
// ✅ 子组件内部处理点击,不需要 useCallback
return (
<li
onClick={() => onSelect(user.id)}
className={isSelected ? 'bg-blue-100' : ''}
>
{user.name}
</li>
);
}
function UserList({ users, onSelect }: {
users: Array<{ id: string; name: string }>;
onSelect: (id: string) => void;
}) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
onSelect(id);
}, [onSelect]);
return (
<ul>
{users.map(user => (
<UserItem
key={user.id}
user={user}
onSelect={handleSelect}
isSelected={selectedId === user.id}
/>
))}
</ul>
);
}最佳实践
✅ 推荐模式
- 函数传递给 memo 优化子组件时使用
- 函数作为 useEffect/useMemo 依赖时使用
- 使用 ESLint exhaustive-deps 规则检查依赖
- 优先使用函数式更新减少依赖
- 考虑使用 useReducer 消除函数依赖
- 在列表渲染中使用子组件而非内联函数
❌ 避免模式
- 不要对本地使用的函数使用 useCallback
- 不要忽略依赖数组的警告
- 不要依赖频繁变化的对象/数组引用
- 不要在 JSX 中使用内联箭头函数
- 不要为了"最佳实践"而无脑使用
- 不要在 useCallback 中执行副作用
📊 useCallback vs useMemo vs 直接声明
| 场景 | 直接声明 | useCallback | useMemo |
|---|---|---|---|
| 本地事件处理 | ✅ 推荐 | ❌ 过度 | ❌ 错误 |
| 传递给 memo 子组件 | ⚠️ 可能失效 | ✅ 推荐 | ⚠️ 间接实现 |
| 作为 useEffect 依赖 | ⚠️ 不稳定 | ✅ 推荐 | ✅ 等价 |
| 缓存计算结果 | ❌ 不适用 | ❌ 错误 | ✅ 推荐 |