摸鱼仔无事,翻了翻项目代码,发现 useMemo / useCallback 使用相对较少,作为性能优化得关键hooks,不能不深入了解下它们。
今天这篇文章不讲 API 表面,而是:
为什么需要它们?什么时候该用?不用会发生什么?
总览
- useMemo:缓存「计算结果」
- useCallback:缓存「函数引用」
- 它们解决的不是“性能慢”,而是:
无意义的重新计算 / 无意义的重新渲染
1️. 一个真实的问题场景
我们先从一个完全真实、你一定写过的组件开始。
function ExpensiveList({ list }) {
const total = list.reduce((sum, item) => sum + item.price, 0);
console.log("ExpensiveList render");
return
}
父组件:
export default function App() {
const [count, setCount] = useState(0);
const list = [
{ id: 1, price: 100 },
{ id: 2, price: 200 },
];
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1
</>
);
}
问题来了 ❗
我只点了
count + 1,为什么 ExpensiveList 也 render 了?
答案:
因为 父组件重新 render,子组件默认一定 render
2️. useMemo:缓存的是“计算结果”
问题本质
每一次 render:
list.reduce(...)
都会重新执行 ——
即使
list根本没变
用 useMemo 改造子组件
import { useMemo } from "react";
function ExpensiveList({ list }: { list: { price: number }[] }) {
// const total = list.reduce((sum, item) => sum + item.price, 0);
const total = useMemo(() => {
console.log("代码执行");
return list.reduce((sum, item) => sum + item.price, 0);
}, [list]);
console.log("ExpensiveList render");
return <div>total: {total}</div>;
}
export default ExpensiveList;
点击父组件button,发生了什么?
- render 仍然会发生
- console.log("代码执行")依然执行了,并没有被缓存
- useMemo缓存失效?
为什么?
分析原因为每次点击,父组件必然重新render, const list = [
{ id: 1, price: 100 },
{ id: 2, price: 200 },
]; 每一次 render,list 都是新引用,那么怎么解决这个问题尼?
改造父组件代码
const list = useMemo(
() => [
{ id: 1, price: 100 },
{ id: 2, price: 200 },
],
[]
);
然后我们再点击button
- 控制台依然render
- useMomo对函数计算结果起到了缓存,未打印 console.log("代码执行");
⚠️ useMemo 不阻止 render,只缓存结果
3. useCallback:缓存的是“函数本身”
一个经典翻车现场
function Child({ onClick }) {
console.log("Child render");
return child btn;
}
父组件:
export default function App() {
const [count2, setCount2] = useState(0);
const handleClick = () => {
console.log("click");
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1
</>
);
}
现象
点
+1,Child 每次都 render
原因
函数也是对象
() => {} !== () => {}
React中,父组件重新执行(render) → JSX 重新创建 → 子组件函数被重新调用(render)
useCallback 登场
const handleClick = useCallback(() => {
console.log("click");
}, []);
结果
- 函数引用稳定
- Child 还是会render,因为组件更新,子组件一定render (感觉好绕)
4. useCallback + React.memo 才是完全体
const Child = React.memo(function Child({ handleClick }: { handleClick: () => void }) {
console.log("Child render");
return <button onClick={ handleClick}>child btn</button>;
});
export default Child;
可以看到,当我们父组件state变化得时候,count2变化,父组件render,子组件并没有render
注:
单独 useCallback 没用
useCallback + memo (React.memo) 才能阻止子组件 render
5. 闭包陷阱:useCallback 最容易踩的坑
const handleClick = useCallback(() => {
console.log(count);
}, []);
永远是初始值,其实会发现useEffect,useCallback,useMemo写法使用上还挺像得
为什么?
useCallback 缓存的是:
第一次 render 的函数快照
正确写法 ①:加依赖
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
正确写法 ②:函数式更新(最推荐)
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
函数式更新 = 闭包终结者
6. useMemo vs useCallback 本质对比
| hook | 缓存的是什么 | 返回值 |
|---|---|---|
| useMemo | 计算结果 | 任意值 |
| useCallback | 函数 | 函数本身(配合React.memo使用) |
用法总结
useCallback(fn) === useMemo(() => fn) useCallback缓存函数,useMemo缓存计算结果
7. 什么时候不该用?
小组件 没有传给 memo 子组件 计算非常轻
过度 useMemo = 性能负优化
8. 最终总结
- useMemo 解决的是:重复计算
- useCallback 解决的是:函数引用不稳定
- render ≠ 重新计算
- 闭包问题来自“渲染快照”
- 函数式更新可以绕开依赖地狱
🔚 面试必杀句
useCallback 是为了解决什么问题?
为了保证函数引用稳定,配合 memo 避免子组件无意义重新渲染,同时要注意闭包和依赖问题。
-
useCallback几乎总是和React.memo成对出现 -
useMemo不依赖React.memo才能生效,可单独使用,useMemo(()=>{},value),value值不变,就不会重新计算 -
render 优化和计算缓存是两条完全不同的优化路径