在 React 函数组件中,每次渲染都会重新执行整个函数体——所有变量、计算、函数声明都会重新创建。这在大多数情况下并没有问题,但当某些计算开销较大,或者某些函数引用被用作子组件的 props 导致子组件无谓重渲染时,就需要对值和函数进行缓存。useMemo 和 useCallback 正是为此而生。
useMemo:缓存计算结果
useMemo 用于缓存一个值。它接收一个“计算函数”和一个依赖数组。只有当依赖项发生变化时,它才会重新执行计算函数并返回新值;否则会直接返回之前缓存的值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
典型场景:
- 避免重复的高开销计算
比如对大列表进行排序、过滤、复杂数学运算等。如果这些计算在每次渲染时都执行,即使相关状态未变,也会浪费资源。
function ProductList({ products, filterKeyword }) {
// 每次 render 都过滤,即使 keyword 和 products 没变
const filtered = useMemo(() => {
return products.filter(p => p.name.includes(filterKeyword));
}, [products, filterKeyword]);
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
在上面的例子中,只有当 products 或 filterKeyword 真正变化时,才会重新执行过滤,否则直接复用上次的结果。
- 作为其他 Hook 的依赖项时保持引用稳定
如果一个派生值会被用在 useEffect 或 useMemo 的依赖数组中,保持其引用稳定可以避免副作用或后续计算的频繁触发。
useCallback:缓存函数引用
useCallback 用于缓存一个函数。它的签名与 useMemo 相似,只不过缓存的是函数本身。
const memoizedCallback = useCallback(
() => { doSomething(a, b); },
[a, b],
);
实际上,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
典型场景:
- 将函数传给使用了
React.memo的子组件
如果子组件通过 React.memo 包装了,那么它会浅比较 props 是否变化。如果父组件在每次渲染时都创建新的函数引用(即使逻辑相同),子组件就会认为 props 发生了变化,从而不必要地重渲染。使用 useCallback 可以保持函数引用不变,配合 React.memo 避免子组件无谓更新。
const Parent = () => {
const [count, setCount] = useState(0);
// 不使用 useCallback:每次 render 都会创建新函数引用
// <Child onClick={() => setCount(c => c + 1)} />
// 使用 useCallback:只有当依赖变化时才会生成新函数
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 空依赖表示函数永远不变
return <Child onIncrement={handleClick} />;
};
const Child = React.memo(({ onIncrement }) => {
console.log('Child 渲染');
return <button onClick={onIncrement}>+1</button>;
});
- 作为其他 Hook 的依赖项(如
useEffect)
如果一个函数在 useEffect 内部被调用,并且你把它写在了依赖数组中,那么稳定的函数引用可以避免副作用不必要的重复执行。
二者关系与选择总结
| 特性 | useMemo | useCallback |
|------|-----------|---------------|
| 缓存目标 | 任意值(对象、数组、原始值) | 函数引用 |
| 本质 | useMemo(() => value, deps) | useMemo(() => fn, deps) |
| 主要用途 | 跳过重计算、保持引用 | 保持函数引用不变,减少子组件渲染 |
| 与 React.memo 协作 | 也可缓存对象/数组 Props | 直接用于缓存回调函数 Props |
重要提醒:不要过早优化
useMemo 和 useCallback 本身也有开销(存储缓存、依赖比较)。对于大多数简单计算和大多数小型函数,直接创建反而更快。滥用它们不仅让代码变复杂,还可能降低性能。
最佳实践:
- 先写出清晰、正确的代码,不添加任何记忆化。
- 当通过 React DevTools Profiler 发现确实存在性能瓶颈(如列表渲染卡顿、高频重渲染)时,再针对性地使用
useMemo/useCallback。 - 重点关注传递给子组件的引用类型 props 和高开销计算,而不是对每一个变量都加缓存。
常见陷阱与避坑
- 依赖数组不完整
忘记声明依赖会导致缓存永远不会更新,形成“过期闭包”。请严格遵循 ESLint 的 react-hooks/exhaustive-deps 规则。
- 滥用导致代码可读性下降
无脑包裹 useMemo/useCallback 会让代码变得晦涩。先确认问题再动手。
- 用
useMemo替代useState+useEffect
不要把 useMemo 当作“根据 props 计算 state”的手段,派生状态通常应该直接计算,或通过 useState 配合 useEffect 来控制(视复杂度而定)。
实用示例:合并使用
function UserList({ users, sortOrder }) {
// 1. 缓存排序后的列表
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) =>
sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
);
}, [users, sortOrder]);
// 2. 缓存删除回调,保持引用不变
const handleDelete = useCallback((id) => {
// 调用外部删除方法
onDeleteUser(id);
}, [onDeleteUser]); // 假设 onDeleteUser 是外部传入并已稳定的引用
return (
<ul>
{sortedUsers.map(user => (
<UserItem
key={user.id}
user={user}
onDelete={handleDelete}
/>
))}
</ul>
);
}
在这个例子中,sortedUsers 避免了每次渲染都重新排序(假设 users 很大),handleDelete 保持引用不变以便传递给 React.memo 优化的 UserItem。
小结
useMemo 和 useCallback 是 React 提供的缓存工具,用于精细化控制值和函数的再生成时机。它们本身并不神奇,而是在明确性能瓶颈后的一剂“优化药”。记住:先让代码正确运行,再在关键路径上做记忆化。