文章目录
在 React 开发中,性能优化始终是一个重要话题。随着应用复杂度的增加,不必要的重新渲染和重复计算会严重影响用户体验。React 提供了两个强大的 Hook —— useMemo
和 useCallback
来帮助解决这些问题。虽然它们都用于性能优化,但各有不同的使用场景和目的。
什么是 useMemo?
useMemo
是一个用于缓存计算结果的 Hook。它会在依赖项发生变化时才重新计算值,否则返回上次缓存的结果。
基本语法
const memoizedValue = useMemo(() => {
// 执行昂贵的计算
return expensiveCalculation(a, b);
}, [a, b]); // 依赖数组
使用场景
- 昂贵的计算操作:避免在每次渲染时重复执行复杂计算
- 避免创建新的对象或数组:防止子组件不必要的重新渲染
实际例子
import React, { useMemo, useState } from 'react';
function ExpensiveComponent({ items, filter }) {
const [count, setCount] = useState(0);
// 没有使用 useMemo - 每次渲染都会重新计算
const expensiveValue = items
.filter(item => item.category === filter)
.reduce((sum, item) => sum + item.price, 0);
// 使用 useMemo - 只有当 items 或 filter 改变时才重新计算
const memoizedValue = useMemo(() => {
console.log('重新计算昂贵的值');
return items
.filter(item => item.category === filter)
.reduce((sum, item) => sum + item.price, 0);
}, [items, filter]);
return (
<div>
<p>计算结果: {memoizedValue}</p>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
点击计数 (不会触发重新计算)
</button>
</div>
);
}
什么是 useCallback?
useCallback
是一个用于缓存函数的 Hook。它返回一个记忆化的回调函数,只有在依赖项发生变化时才会返回新的函数。
基本语法
const memoizedCallback = useCallback(() => {
// 函数逻辑
doSomething(a, b);
}, [a, b]); // 依赖数组
使用场景
- 传递给子组件的回调函数:避免子组件因为新函数引用而重新渲染
- 作为其他 Hook 的依赖:确保依赖数组的稳定性
实际例子
import React, { useCallback, useState, memo } from 'react';
// 子组件使用 memo 包装,只有 props 改变时才重新渲染
const ChildComponent = memo(({ onClick, name }) => {
console.log(`${name} 组件渲染了`);
return <button onClick={onClick}>点击 {name}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 没有使用 useCallback - 每次渲染都会创建新函数
const handleClick1 = () => {
console.log('点击了按钮1');
};
// 使用 useCallback - 函数引用保持稳定
const handleClick2 = useCallback(() => {
console.log('点击了按钮2');
}, []); // 空依赖数组,函数永远不会改变
// 依赖于 count 的回调函数
const handleClick3 = useCallback(() => {
console.log(`当前计数: ${count}`);
}, [count]); // 只有 count 改变时才创建新函数
return (
<div>
<p>计数: {count}</p>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入名称"
/>
<button onClick={() => setCount(count + 1)}>增加计数</button>
{/* 每次父组件重新渲染时,子组件也会重新渲染 */}
<ChildComponent onClick={handleClick1} name="按钮1(未优化)" />
{/* 只有当 handleClick2 改变时,子组件才会重新渲染 */}
<ChildComponent onClick={handleClick2} name="按钮2(已优化)" />
{/* 只有当 count 改变时,子组件才会重新渲染 */}
<ChildComponent onClick={handleClick3} name="按钮3(依赖count)" />
</div>
);
}
核心区别对比
特性 | useMemo | useCallback |
---|---|---|
缓存对象 | 缓存计算结果(值) | 缓存函数本身 |
返回值 | 返回计算后的值 | 返回记忆化的函数 |
主要用途 | 避免重复计算 | 避免函数重新创建 |
典型场景 | 复杂计算、数据处理 | 事件处理器、回调函数 |
性能影响 | 减少计算开销 | 减少子组件重新渲染 |
什么时候使用它们?
使用 useMemo 的时机
- 计算开销很大:复杂的数学运算、大数据处理
- 创建复杂对象:避免每次渲染都创建新的对象或数组
- 作为其他 Hook 的依赖:确保依赖的稳定性
// ✅ 适合使用 useMemo
const expensiveCalculation = useMemo(() => {
return heavyProcessing(largeDataSet);
}, [largeDataSet]);
// ✅ 避免创建新对象
const userInfo = useMemo(() => ({
name: user.name,
email: user.email,
isActive: user.status === 'active'
}), [user.name, user.email, user.status]);
使用 useCallback 的时机
- 传递给子组件的函数:特别是使用了
React.memo
的子组件 - 作为 useEffect 的依赖:避免 effect 不必要的重新执行
- 自定义 Hook 中的函数:保持 API 的稳定性
// ✅ 适合使用 useCallback
const handleSubmit = useCallback((formData) => {
submitForm(formData);
}, []);
// ✅ 作为 useEffect 的依赖
const fetchData = useCallback(async () => {
const data = await api.getData(id);
setData(data);
}, [id]);
useEffect(() => {
fetchData();
}, [fetchData]);
常见误区和注意事项
误区 1:过度使用
不是所有的计算都需要使用 useMemo
,也不是所有的函数都需要使用 useCallback
。这些 Hook 本身也有开销,只有在确实需要优化时才使用。
// ❌ 不需要优化的简单计算
const simpleValue = useMemo(() => a + b, [a, b]);
// ✅ 直接计算即可
const simpleValue = a + b;
误区 2:依赖数组不正确
确保依赖数组包含所有使用到的变量,否则可能导致 bug。
// ❌ 缺少依赖
const memoizedValue = useMemo(() => {
return calculate(a, b, c);
}, [a, b]); // 缺少 c
// ✅ 完整的依赖
const memoizedValue = useMemo(() => {
return calculate(a, b, c);
}, [a, b, c]);
误区 3:在条件语句中使用
Hook 必须在函数组件的顶层调用,不能在条件语句中使用。
// ❌ 错误的使用方式
if (condition) {
const value = useMemo(() => calculate(), []);
}
// ✅ 正确的使用方式
const value = useMemo(() => {
if (condition) {
return calculate();
}
return defaultValue;
}, [condition]);
实际应用示例
让我们看一个综合使用 useMemo
和 useCallback
的实际例子:
import React, { useState, useMemo, useCallback, memo } from 'react';
// 模拟昂贵的计算函数
const expensiveCalculation = (items) => {
console.log('执行昂贵计算...');
return items.reduce((sum, item) => sum + item.value * item.quantity, 0);
};
// 子组件
const ProductItem = memo(({ product, onUpdate, onDelete }) => {
console.log(`渲染产品: ${product.name}`);
return (
<div>
<span>{product.name}: {product.value} x {product.quantity}</span>
<button onClick={() => onUpdate(product.id)}>更新</button>
<button onClick={() => onDelete(product.id)}>删除</button>
</div>
);
});
function ShoppingCart() {
const [products, setProducts] = useState([
{ id: 1, name: '商品A', value: 100, quantity: 2 },
{ id: 2, name: '商品B', value: 200, quantity: 1 },
{ id: 3, name: '商品C', value: 50, quantity: 3 }
]);
const [discount, setDiscount] = useState(0);
// 使用 useMemo 缓存总价计算
const totalPrice = useMemo(() => {
const basePrice = expensiveCalculation(products);
return basePrice * (1 - discount / 100);
}, [products, discount]);
// 使用 useCallback 缓存事件处理函数
const handleUpdateProduct = useCallback((productId) => {
setProducts(prev => prev.map(p =>
p.id === productId
? { ...p, quantity: p.quantity + 1 }
: p
));
}, []);
const handleDeleteProduct = useCallback((productId) => {
setProducts(prev => prev.filter(p => p.id !== productId));
}, []);
return (
<div>
<h2>购物车</h2>
{products.map(product => (
<ProductItem
key={product.id}
product={product}
onUpdate={handleUpdateProduct}
onDelete={handleDeleteProduct}
/>
))}
<div>
<label>
折扣(%):
<input
type="number"
value={discount}
onChange={(e) => setDiscount(Number(e.target.value))}
/>
</label>
</div>
<h3>总价: ¥{totalPrice.toFixed(2)}</h3>
</div>
);
}