先说结论
- 为了计算性能 :遇到大数组遍历 、复杂算法 -> 用
useMemo。 - 为了引用稳定:
-
- 要传给
React.memo包裹的子组件的对象/数组 -> 用useMemo。 - 要传给
React.memo包裹的子组件的函数 -> 用useCallback。 - 函数或对象要放进
useEffect的依赖数组 里 -> 用useMemo/useCallback。
- 要传给
除此之外,直接写原生 JS 代码即可。
一、 什么时候使用 useMemo?
useMemo 的作用是缓存计算结果。
1. 进行昂贵的计算(Expensive Calculation)时
如果你的组件内部有一个计算过程非常耗时(例如:遍历数万条数据、复杂的数学运算、图像处理),你不希望每次组件重新渲染(比如只是输入框打字)都重新计算一遍。
- 标准:如果计算耗时超过 1ms(肉眼不可见,但在低端设备累积会卡顿),或者处理数组长度很大。
- 代码示例:
ini
// ❌ 每次 render 都会运行,导致卡顿
const filteredList = hugeList.filter(item => item.includes(query));
// ✅ 只有当 hugeList 或 query 变化时才重新计算
const filteredList = useMemo(() => {
return hugeList.filter(item => item.includes(query));
}, [hugeList, query]);
2. 防止子组件不必要的渲染(引用稳定性)
这是最常见但也最容易被忽略的用法。
在 JavaScript 中,{ a: 1 } !== { a: 1 }(每次创建的对象引用地址不同)。
如果你的子组件使用了 React.memo 包裹,但你传给它的 props 是一个在父组件中动态生成的对象 或数组 ,那么 React.memo 会失效,因为每次父组件渲染,生成的对象引用都变了。
- 代码示例:
javascript
const Parent = () => {
// ❌ 每次 Parent 渲染,config 都是一个新的对象引用
// 导致 Child 认为 props 变了,从而强制重新渲染
const config = { color: 'red' };
// ✅ 缓存了对象引用,只有依赖变了引用才变
const memoConfig = useMemo(() => ({ color: 'red' }), []);
return <Child config={memoConfig} />;
};
// Child 被 memo 包裹
const Child = React.memo(({ config }) => { ... });
二、 什么时候使用 useCallback?
useCallback 的作用是缓存函数引用。
它的核心用途只有一个 :维持函数引用的稳定性,以配合 React.memo 或 useEffect 使用。
1. 将函数传递给经过优化的子组件(React.memo)
这是 useCallback 90% 的使用场景。
如果你把一个函数传给子组件,而子组件用了 React.memo,你不希望父组件渲染时,因为"函数是新创建的"而导致子组件也跟着渲染。
- 代码示例:
javascript
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ 每次 Parent 渲染,handleClick 都是一个全新的函数指针
// 导致 BigList 组件的 props 发生变化,破坏了 React.memo 的效果
const handleClick = () => {
console.log('Clicked');
};
// ✅ 缓存函数引用,只要依赖不变,handleClick 永远是同一个引用
const memoHandleClick = useCallback(() => {
console.log('Clicked');
}, []);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>加一</button>
{/* 如果不传 memoHandleClick,这里的 memo 就白写了 */}
<BigList onItemClick={memoHandleClick} />
</>
);
};
const BigList = React.memo(({ onItemClick }) => {
console.log('BigList Rendered'); // 使用 useCallback 后,点击"加一"不会触发这里
return <div>List...</div>;
});
2. 函数作为 useEffect 的依赖项时
如果你的 useEffect 依赖于外部传入的一个函数,为了避免 useEffect 无限循环或者频繁触发,这个函数必须被 useCallback 包裹。
- 代码示例:
scss
function ChatRoom({ roomId, createConnection }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
// 如果 createConnection 没有被 useCallback 包裹,
// 每次父组件渲染都会导致这里重跑,连接断开又重连
}, [roomId, createConnection]);
}
三、 什么时候不需要?(避坑指南)
很多新手会把所有函数和变量都包上,这是错误的。
- 简单的计算:
ini
// ❌ 没必要,useMemo 本身也有开销(内存分配、依赖比较)
const total = useMemo(() => price * quantity, [price, quantity]);
// ✅ 直接算就好,JS 引擎算这种东西极快
const total = price * quantity;
- 子组件没有使用
React.memo:
如果子组件只是一个普通的div或者没有用memo包裹的组件,你给它传useCallback包裹的函数是完全浪费的。因为不管 props 变没变,子组件都会跟着父组件一起重新渲染。 - 仅仅为了"看起来优化了" :
useMemo和useCallback会占用额外的内存,并且 React 需要在每次渲染时对比依赖数组。滥用会导致性能更差,代码可读性降低。