问题背景
useCallback 和 useMemo 是 React 提供的两个性能优化 Hook,它们看起来很相似,但用途和返回值有本质区别。
核心区别
语法对比
jsx// useCallback:返回函数本身 const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); // useMemo:返回函数执行结果 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
本质区别
| 特性 | useCallback | useMemo |
|---|---|---|
| 返回值 | 函数引用 | 任意值(计算结果) |
| 用途 | 缓存函数 | 缓存计算结果 |
| 类似于 | 函数记忆化 | 值记忆化 |
| 场景 | 避免函数重建 | 避免重复计算 |
useCallback 详解
基本用法
jsxconst memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b] );
使用场景
1. 传递给子组件避免不必要渲染
jsxfunction Parent({ items }) { // ❌ 每次渲染都创建新函数,导致 Child 重新渲染 const handleClick = () => { console.log("clicked"); }; // ✅ 使用 useCallback 缓存函数 const handleClick = useCallback(() => { console.log("clicked"); }, []); return <Child onClick={handleClick} items={items} />; } // 配合 React.memo 使用 const Child = React.memo(({ onClick, items }) => { console.log("Child render"); return <button onClick={onClick}>Click</button>; });
2. 作为 useEffect 依赖
jsxfunction UserProfile({ userId }) { // ❌ fetchUser 每次都是新引用,useEffect 每次都执行 const fetchUser = () => { api.getUser(userId); }; // ✅ 使用 useCallback 稳定函数引用 const fetchUser = useCallback(() => { api.getUser(userId); }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]); }
useMemo 详解
基本用法
jsxconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
使用场景
1. 避免重复计算
jsxfunction ProductList({ products, filter }) { // ❌ 每次渲染都重新过滤 const filteredProducts = products.filter(p => p.name.includes(filter) ); // ✅ 只在 products 或 filter 变化时重新计算 const filteredProducts = useMemo(() => products.filter(p => p.name.includes(filter)), [products, filter] ); return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>; }
2. 复杂计算优化
jsxfunction DataTable({ data }) { // ✅ 大数据量排序只计算一次 const sortedData = useMemo(() => { console.log("Sorting..."); return [...data].sort((a, b) => a.score - b.score); }, [data]); // ✅ 复杂数据转换 const chartData = useMemo(() => { return data.reduce((acc, item) => { // 复杂聚合逻辑 return acc; }, {}); }, [data]); return <Chart data={chartData} />; }
3. 引用稳定性
jsxfunction Parent({ items }) { // ❌ 每次渲染创建新对象,导致子组件重渲染 const style = { color: "red" }; // ✅ 保持对象引用稳定 const style = useMemo(() => ({ color: "red" }), []); return <Child style={style} />; }
组合使用
useCallback 本质上是 useMemo 的语法糖
jsx// useCallback 实现 useCallback(fn, deps); // 等价于 useMemo(() => fn, deps);
配合使用优化性能
jsxfunction SearchResults({ query, data }) { // 缓存过滤结果 const results = useMemo(() => { return data.filter(item => item.name.toLowerCase().includes(query.toLowerCase()) ); }, [data, query]); // 缓存事件处理函数 const handleItemClick = useCallback((id) => { console.log("Selected:", id); }, []); // 缓存传递给子组件的 props const listProps = useMemo(() => ({ items: results, onItemClick: handleItemClick }), [results, handleItemClick]); return <ResultList {...listProps} />; }
常见误区
误区 1:过度使用
jsx// ❌ 不必要的优化,简单计算不需要 useMemo const total = useMemo(() => a + b, [a, b]); // ✅ 直接计算即可 const total = a + b;
误区 2:忽略依赖
jsx// ❌ 闭包陷阱 const multiplier = 2; const result = useMemo(() => value * multiplier, [value]); // ✅ 添加所有依赖 const multiplier = 2; const result = useMemo(() => value * multiplier, [value, multiplier]);
最佳实践
- 先测量,后优化:使用 React DevTools Profiler 找出性能瓶颈
- 简单计算不需要缓存:缓存本身也有开销
- 配合 React.memo 使用:确保子组件实现了 shouldComponentUpdate
- 正确声明依赖:遵循 ESLint 提示
jsx// 好的组合示例 function UserList({ users, onSelect }) { // 缓存排序结果 const sortedUsers = useMemo( () => [...users].sort((a, b) => a.name.localeCompare(b.name)), [users] ); // 缓存事件处理 const handleSelect = useCallback( (userId) => onSelect(userId), [onSelect] ); return ( <ul> {sortedUsers.map(user => ( <UserItem key={user.id} user={user} onSelect={handleSelect} /> ))} </ul> ); }