Appearance
useMemo计算缓存
学习目标
通过本章学习,你将全面掌握:
- useMemo的概念和工作原理
- useMemo的使用场景
- useMemo vs 重新计算的权衡
- useMemo的性能优化技巧
- 依赖数组的正确使用
- 常见错误和陷阱
- useMemo的实际应用案例
- React 19中的useMemo增强
第一部分:useMemo基础
1.1 什么是useMemo
useMemo是React提供的性能优化Hook,用于缓存计算结果,避免在每次渲染时都重新计算。
jsx
import { useMemo } from 'react';
// 基本语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 示例
function ExpensiveCalculation({ items }) {
// 没有useMemo:每次渲染都计算
const total = items.reduce((sum, item) => sum + item.value, 0);
// 使用useMemo:只在items变化时计算
const totalMemo = useMemo(() => {
console.log('计算总和');
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
return <div>总计:{totalMemo}</div>;
}1.2 useMemo的工作原理
jsx
// useMemo的简化实现
function useMemoSimplified(factory, deps) {
// 获取当前Hook
const hook = getCurrentHook();
// 检查依赖是否变化
const hasChanged = !hook || !areDepsEqual(hook.deps, deps);
if (hasChanged) {
// 依赖变化,重新计算
hook.value = factory();
hook.deps = deps;
}
// 返回缓存的值
return hook.value;
}
// 依赖比较(浅比较)
function areDepsEqual(prevDeps, nextDeps) {
if (prevDeps === null) return false;
if (prevDeps.length !== nextDeps.length) return false;
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(prevDeps[i], nextDeps[i])) {
continue;
}
return false;
}
return true;
}1.3 基本用法示例
jsx
function BasicUseMemo() {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
const [filter, setFilter] = useState('all');
// 昂贵的过滤计算
const filteredNumbers = useMemo(() => {
console.log('执行过滤计算');
if (filter === 'even') {
return numbers.filter(n => n % 2 === 0);
} else if (filter === 'odd') {
return numbers.filter(n => n % 2 !== 0);
}
return numbers;
}, [numbers, filter]);
// 昂贵的求和计算
const sum = useMemo(() => {
console.log('执行求和计算');
return filteredNumbers.reduce((a, b) => a + b, 0);
}, [filteredNumbers]);
return (
<div>
<div>
<button onClick={() => setFilter('all')}>全部</button>
<button onClick={() => setFilter('even')}>偶数</button>
<button onClick={() => setFilter('odd')}>奇数</button>
</div>
<p>过滤后: {filteredNumbers.join(', ')}</p>
<p>总和: {sum}</p>
<button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
添加数字
</button>
</div>
);
}第二部分:useMemo的使用场景
2.1 场景1:昂贵的计算
jsx
function ExpensiveComputation({ data }) {
// 不使用useMemo:每次渲染都执行复杂计算
const processedData = data.map(item => ({
...item,
processed: complexCalculation(item) // 假设这是昂贵的计算
})).sort((a, b) => b.processed - a.processed);
// 使用useMemo:只在data变化时计算
const processedDataMemo = useMemo(() => {
console.log('执行复杂计算');
return data
.map(item => ({
...item,
processed: complexCalculation(item)
}))
.sort((a, b) => b.processed - a.processed);
}, [data]);
return (
<div>
{processedDataMemo.map(item => (
<div key={item.id}>{item.processed}</div>
))}
</div>
);
}
// 模拟复杂计算
function complexCalculation(item) {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += item.value * Math.random();
}
return result;
}2.2 场景2:避免重新创建对象/数组
jsx
function ObjectCreation({ userId }) {
const [count, setCount] = useState(0);
// 不使用useMemo:每次渲染创建新对象
const userConfig = {
id: userId,
settings: {
theme: 'dark',
language: 'zh-CN'
}
};
// 使用useMemo:只在userId变化时创建新对象
const userConfigMemo = useMemo(() => ({
id: userId,
settings: {
theme: 'dark',
language: 'zh-CN'
}
}), [userId]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
{/* userConfig每次都是新对象,Child会重新渲染 */}
<Child config={userConfig} />
{/* userConfigMemo引用稳定,Child不会重新渲染 */}
<MemoChild config={userConfigMemo} />
</div>
);
}
const Child = ({ config }) => {
console.log('Child渲染');
return <div>{config.id}</div>;
};
const MemoChild = React.memo(({ config }) => {
console.log('MemoChild渲染');
return <div>{config.id}</div>;
});2.3 场景3:作为其他Hook的依赖
jsx
function UseMemoAsDependency({ userId }) {
const [data, setData] = useState(null);
// 不使用useMemo:options每次都变,useEffect每次都执行
const options = {
userId,
timestamp: Date.now()
};
// 使用useMemo:options只在userId变化时变化
const optionsMemo = useMemo(() => ({
userId,
timestamp: Date.now()
}), [userId]);
// 不好的做法
useEffect(() => {
fetchData(options).then(setData);
}, [options]); // options每次都是新对象,Effect每次都执行
// 好的做法
useEffect(() => {
fetchData(optionsMemo).then(setData);
}, [optionsMemo]); // optionsMemo只在userId变化时变化
return <div>{data?.name}</div>;
}2.4 场景4:大列表过滤和排序
jsx
function FilteredList({ items, searchTerm, sortBy }) {
// 过滤
const filteredItems = useMemo(() => {
console.log('过滤列表');
return items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [items, searchTerm]);
// 排序
const sortedItems = useMemo(() => {
console.log('排序列表');
return [...filteredItems].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else if (sortBy === 'price') {
return a.price - b.price;
}
return 0;
});
}, [filteredItems, sortBy]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>
{item.name} - ¥{item.price}
</li>
))}
</ul>
);
}2.5 场景5:复杂的派生状态
jsx
function DerivedState({ users, orders }) {
// 计算每个用户的订单统计
const userOrderStats = useMemo(() => {
console.log('计算用户订单统计');
const stats = {};
users.forEach(user => {
stats[user.id] = {
totalOrders: 0,
totalAmount: 0,
averageAmount: 0
};
});
orders.forEach(order => {
if (stats[order.userId]) {
stats[order.userId].totalOrders++;
stats[order.userId].totalAmount += order.amount;
}
});
Object.keys(stats).forEach(userId => {
const stat = stats[userId];
stat.averageAmount = stat.totalOrders > 0
? stat.totalAmount / stat.totalOrders
: 0;
});
return stats;
}, [users, orders]);
return (
<div>
{users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>订单数: {userOrderStats[user.id].totalOrders}</p>
<p>总金额: ¥{userOrderStats[user.id].totalAmount}</p>
<p>平均金额: ¥{userOrderStats[user.id].averageAmount.toFixed(2)}</p>
</div>
))}
</div>
);
}第三部分:useMemo vs 重新计算
3.1 性能权衡
jsx
// 什么时候不需要useMemo
function SimpleCalculation({ a, b }) {
// 简单计算,不需要useMemo
const sum = a + b; // 很快
const product = a * b; // 很快
const isEven = a % 2 === 0; // 很快
return (
<div>
<p>和: {sum}</p>
<p>积: {product}</p>
<p>偶数: {isEven ? '是' : '否'}</p>
</div>
);
}
// 什么时候需要useMemo
function ComplexCalculation({ data }) {
// 复杂计算,需要useMemo
const result = useMemo(() => {
let sum = 0;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < 1000; j++) {
sum += data[i] * Math.sqrt(j);
}
}
return sum;
}, [data]);
return <div>{result}</div>;
}3.2 过度使用useMemo的问题
jsx
// 不好:过度使用useMemo
function OverUseMemo({ name, age }) {
// 简单字符串拼接不需要memo
const greeting = useMemo(() => `Hello, ${name}`, [name]); // 不必要
// 简单布尔运算不需要memo
const isAdult = useMemo(() => age >= 18, [age]); // 不必要
// 简单对象,如果每次渲染都需要新的,也不需要memo
const style = useMemo(() => ({ color: 'red' }), []); // 不必要
return (
<div>
<p>{greeting}</p>
<p>{isAdult ? '成年' : '未成年'}</p>
</div>
);
}
// 好:合理使用useMemo
function ProperUseMemo({ items }) {
// 复杂计算才使用memo
const total = useMemo(() => {
return items.reduce((sum, item) => {
// 假设这里有复杂的计算
return sum + expensiveCalculation(item);
}, 0);
}, [items]);
return <div>总计: {total}</div>;
}3.3 性能测量
jsx
function PerformanceComparison({ data }) {
const [withMemo, setWithMemo] = useState(true);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
// 不使用memo的版本
const withoutMemoResult = (() => {
const start = performance.now();
const result = data.reduce((sum, item) => sum + item, 0);
const end = performance.now();
console.log('不使用memo:', end - start, 'ms');
return result;
})();
// 使用memo的版本
const withMemoResult = useMemo(() => {
const start = performance.now();
const result = data.reduce((sum, item) => sum + item, 0);
const end = performance.now();
console.log('使用memo:', end - start, 'ms');
return result;
}, [data]);
return (
<div>
<p>渲染次数: {renderCount.current}</p>
<p>不使用memo: {withoutMemoResult}</p>
<p>使用memo: {withMemoResult}</p>
</div>
);
}第四部分:依赖数组详解
4.1 正确的依赖数组
jsx
function CorrectDependencies() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
const [c, setC] = useState(3);
// 正确:列出所有使用的值
const result1 = useMemo(() => {
return a + b;
}, [a, b]); // 正确
// 错误:遗漏依赖
const result2 = useMemo(() => {
return a + b;
}, [a]); // 错误:缺少b
// 正确:不使用的值不需要列入
const result3 = useMemo(() => {
return a + b;
}, [a, b]); // 正确:c没有使用,不需要列入
return <div>{result1}</div>;
}4.2 对象和数组依赖
jsx
function ObjectDependencies({ user }) {
// 错误:user是对象,每次都是新的引用
const greeting = useMemo(() => {
return `Hello, ${user.name}`;
}, [user]); // user每次都变,memo失效
// 正确:只依赖使用的属性
const greetingFixed = useMemo(() => {
return `Hello, ${user.name}`;
}, [user.name]); // 只依赖name属性
// 错误:依赖数组中的对象
const config = { theme: 'dark' };
const styled = useMemo(() => {
return { ...config, color: 'red' };
}, [config]); // config每次都是新对象
// 正确:使用稳定的值
const theme = 'dark';
const styledFixed = useMemo(() => {
return { theme, color: 'red' };
}, [theme]);
return <div>{greetingFixed}</div>;
}4.3 函数依赖
jsx
function FunctionDependencies() {
const [count, setCount] = useState(0);
// 问题:函数每次都是新的
const logger = () => {
console.log('Count:', count);
};
const value = useMemo(() => {
logger();
return count * 2;
}, [logger]); // logger每次都变
// 解决方案1:使用useCallback
const loggerMemo = useCallback(() => {
console.log('Count:', count);
}, [count]);
const valueMemo = useMemo(() => {
loggerMemo();
return count * 2;
}, [count, loggerMemo]);
// 解决方案2:在memo内部定义函数
const valueBetter = useMemo(() => {
const logger = () => {
console.log('Count:', count);
};
logger();
return count * 2;
}, [count]);
return <div>{valueBetter}</div>;
}4.4 空依赖数组
jsx
function EmptyDependencies() {
const [count, setCount] = useState(0);
// 空依赖:只计算一次
const constantValue = useMemo(() => {
console.log('只执行一次');
return Math.random();
}, []);
// 警告:使用了count但没有列为依赖
const staleValue = useMemo(() => {
return count * 2; // count变化时不会重新计算
}, []); // 错误:应该包含count
return (
<div>
<p>常量: {constantValue}</p>
<p>过期值: {staleValue}</p>
<p>当前count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
);
}第五部分:常见错误和陷阱
5.1 错误1:依赖项不稳定
jsx
// 错误示例
function UnstableDependency() {
const [count, setCount] = useState(0);
// options每次渲染都是新对象
const options = { multiplier: 2 };
const result = useMemo(() => {
return count * options.multiplier;
}, [count, options]); // options总是变化,memo失效
return <div>{result}</div>;
}
// 正确示例
function StableDependency() {
const [count, setCount] = useState(0);
// 方案1:使用常量
const OPTIONS = { multiplier: 2 };
const result1 = useMemo(() => {
return count * OPTIONS.multiplier;
}, [count]);
// 方案2:只依赖使用的值
const multiplier = 2;
const result2 = useMemo(() => {
return count * multiplier;
}, [count, multiplier]);
return <div>{result2}</div>;
}5.2 错误2:在memo内部修改外部状态
jsx
// 错误示例
function SideEffectInMemo() {
const [count, setCount] = useState(0);
const [log, setLog] = useState([]);
const result = useMemo(() => {
// 错误:在memo中修改状态
setLog([...log, count]); // 副作用!
return count * 2;
}, [count, log, setLog]);
return <div>{result}</div>;
}
// 正确示例
function NoSideEffectInMemo() {
const [count, setCount] = useState(0);
const [log, setLog] = useState([]);
// memo只用于计算
const result = useMemo(() => {
return count * 2;
}, [count]);
// 副作用放在useEffect中
useEffect(() => {
setLog(prev => [...prev, count]);
}, [count]);
return <div>{result}</div>;
}5.3 错误3:忘记返回值
jsx
// 错误示例
function ForgotReturn({ items }) {
const result = useMemo(() => {
items.filter(item => item.active); // 忘记return
}, [items]);
return <div>{result}</div>; // result是undefined
}
// 正确示例
function WithReturn({ items }) {
const result = useMemo(() => {
return items.filter(item => item.active); // 记得return
}, [items]);
// 或使用隐式返回
const result2 = useMemo(() =>
items.filter(item => item.active)
, [items]);
return <div>{result.length}</div>;
}5.4 错误4:在条件语句中使用
jsx
// 错误示例
function ConditionalMemo({ condition, data }) {
let result;
if (condition) {
result = useMemo(() => { // 错误:条件调用Hook
return data.length;
}, [data]);
}
return <div>{result}</div>;
}
// 正确示例
function ProperConditional({ condition, data }) {
// 总是调用useMemo
const result = useMemo(() => {
if (condition) {
return data.length;
}
return 0;
}, [condition, data]);
return <div>{result}</div>;
}第六部分:实战案例
6.1 案例1:数据表格
jsx
function DataTable({ data, sortColumn, sortDirection, filters }) {
// 过滤数据
const filteredData = useMemo(() => {
console.log('执行过滤');
return data.filter(row => {
return Object.keys(filters).every(key => {
const filterValue = filters[key];
if (!filterValue) return true;
return String(row[key]).toLowerCase().includes(filterValue.toLowerCase());
});
});
}, [data, filters]);
// 排序数据
const sortedData = useMemo(() => {
console.log('执行排序');
if (!sortColumn) return filteredData;
return [...filteredData].sort((a, b) => {
const aValue = a[sortColumn];
const bValue = b[sortColumn];
let comparison = 0;
if (aValue > bValue) comparison = 1;
if (aValue < bValue) comparison = -1;
return sortDirection === 'asc' ? comparison : -comparison;
});
}, [filteredData, sortColumn, sortDirection]);
// 分页数据
const [page, setPage] = useState(0);
const pageSize = 10;
const paginatedData = useMemo(() => {
console.log('执行分页');
const start = page * pageSize;
const end = start + pageSize;
return sortedData.slice(start, end);
}, [sortedData, page, pageSize]);
return (
<div>
<table>
<tbody>
{paginatedData.map(row => (
<tr key={row.id}>
<td>{row.name}</td>
<td>{row.value}</td>
</tr>
))}
</tbody>
</table>
<div>
<button onClick={() => setPage(p => Math.max(0, p - 1))}>
上一页
</button>
<span>第 {page + 1} 页</span>
<button onClick={() => setPage(p => p + 1)}>
下一页
</button>
</div>
</div>
);
}6.2 案例2:图表数据处理
jsx
function ChartComponent({ rawData, dateRange, groupBy }) {
// 过滤日期范围
const dateFilteredData = useMemo(() => {
if (!dateRange) return rawData;
return rawData.filter(item => {
const date = new Date(item.date);
return date >= dateRange.start && date <= dateRange.end;
});
}, [rawData, dateRange]);
// 分组聚合
const aggregatedData = useMemo(() => {
const groups = {};
dateFilteredData.forEach(item => {
const key = groupBy === 'day'
? item.date.substring(0, 10)
: groupBy === 'month'
? item.date.substring(0, 7)
: item.date.substring(0, 4);
if (!groups[key]) {
groups[key] = { key, value: 0, count: 0 };
}
groups[key].value += item.value;
groups[key].count++;
});
return Object.values(groups).map(group => ({
...group,
average: group.value / group.count
}));
}, [dateFilteredData, groupBy]);
// 计算统计信息
const statistics = useMemo(() => {
const values = aggregatedData.map(d => d.value);
return {
min: Math.min(...values),
max: Math.max(...values),
average: values.reduce((a, b) => a + b, 0) / values.length,
total: values.reduce((a, b) => a + b, 0)
};
}, [aggregatedData]);
return (
<div>
<div className="statistics">
<p>最小值: {statistics.min}</p>
<p>最大值: {statistics.max}</p>
<p>平均值: {statistics.average.toFixed(2)}</p>
<p>总计: {statistics.total}</p>
</div>
<Chart data={aggregatedData} />
</div>
);
}6.3 案例3:搜索和高亮
jsx
function SearchResults({ items, searchTerm }) {
// 搜索匹配
const searchResults = useMemo(() => {
if (!searchTerm) return items;
const term = searchTerm.toLowerCase();
return items.filter(item =>
item.title.toLowerCase().includes(term) ||
item.description.toLowerCase().includes(term)
).map(item => ({
...item,
score: calculateRelevance(item, term)
})).sort((a, b) => b.score - a.score);
}, [items, searchTerm]);
// 高亮文本
const highlightedResults = useMemo(() => {
if (!searchTerm) return searchResults;
return searchResults.map(item => ({
...item,
highlightedTitle: highlightText(item.title, searchTerm),
highlightedDescription: highlightText(item.description, searchTerm)
}));
}, [searchResults, searchTerm]);
return (
<div>
<p>找到 {highlightedResults.length} 个结果</p>
{highlightedResults.map(item => (
<div key={item.id}>
<h3 dangerouslySetInnerHTML={{ __html: item.highlightedTitle }} />
<p dangerouslySetInnerHTML={{ __html: item.highlightedDescription }} />
</div>
))}
</div>
);
}
function calculateRelevance(item, term) {
let score = 0;
if (item.title.toLowerCase().includes(term)) score += 2;
if (item.description.toLowerCase().includes(term)) score += 1;
return score;
}
function highlightText(text, searchTerm) {
const regex = new RegExp(`(${searchTerm})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}第七部分:性能优化技巧
7.1 组合useMemo和React.memo
jsx
// 子组件
const ExpensiveChild = React.memo(({ data, config }) => {
console.log('ExpensiveChild渲染');
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});
// 父组件
function ParentComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// 使用useMemo缓存数据
const processedData = useMemo(() => {
return items.map(item => ({
...item,
processed: true
}));
}, [items]);
// 使用useMemo缓存配置对象
const config = useMemo(() => ({
theme: 'dark',
pageSize: 10
}), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* 由于data和config引用稳定,ExpensiveChild不会重新渲染 */}
<ExpensiveChild data={processedData} config={config} />
</div>
);
}7.2 避免过度优化
jsx
// 不好:过度优化
function OverOptimized() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// 简单拼接不需要memo
const greeting = useMemo(() => `Hello, ${name}`, [name]);
// 简单判断不需要memo
const isAdult = useMemo(() => age >= 18, [age]);
// 简单计算不需要memo
const nextAge = useMemo(() => age + 1, [age]);
return <div>{greeting}</div>;
}
// 好:合理优化
function WellOptimized({ items }) {
// 只对真正昂贵的计算使用memo
const total = useMemo(() => {
return items.reduce((sum, item) => {
// 假设这里有复杂计算
return sum + complexCalculation(item);
}, 0);
}, [items]);
return <div>总计: {total}</div>;
}7.3 使用Profiler测量
jsx
import { Profiler } from 'react';
function ProfiledComponent() {
const [data, setData] = useState([]);
const expensiveValue = useMemo(() => {
const start = performance.now();
const result = data.reduce((sum, item) => sum + item, 0);
const end = performance.now();
console.log('计算耗时:', end - start, 'ms');
return result;
}, [data]);
const onRenderCallback = (
id,
phase,
actualDuration,
baseDuration
) => {
console.log(`${id} ${phase}阶段耗时: ${actualDuration}ms`);
};
return (
<Profiler id="ExpensiveComponent" onRender={onRenderCallback}>
<div>
<p>结果: {expensiveValue}</p>
</div>
</Profiler>
);
}第八部分:React 19增强
8.1 React 19编译器自动优化
jsx
// React 19的编译器可能自动添加memo
function AutoOptimized({ items }) {
// 编译器会识别这种模式并自动优化
const filtered = items.filter(item => item.active);
const sorted = filtered.sort((a, b) => a.value - b.value);
// 编译器可能会自动转换为:
// const filtered = useMemo(() =>
// items.filter(item => item.active),
// [items]
// );
// const sorted = useMemo(() =>
// filtered.sort((a, b) => a.value - b.value),
// [filtered]
// );
return <List items={sorted} />;
}8.2 使用use() Hook
jsx
import { use, useMemo, Suspense } from 'react';
function DataComponent({ dataPromise }) {
// React 19: use() Hook可以在条件语句中使用
const data = use(dataPromise);
// 结合useMemo处理数据
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: true
}));
}, [data]);
return <div>{processedData.length}</div>;
}
// 使用
<Suspense fallback={<div>加载中...</div>}>
<DataComponent dataPromise={fetchData()} />
</Suspense>第九部分:最佳实践
9.1 何时使用useMemo
jsx
// ✅ 使用useMemo的场景:
// 1. 昂贵的计算
const result = useMemo(() => expensiveCalculation(data), [data]);
// 2. 引用相等性很重要(传给React.memo的组件)
const config = useMemo(() => ({ theme: 'dark' }), []);
// 3. 作为其他Hook的依赖
const options = useMemo(() => ({ filter: 'active' }), []);
useEffect(() => {
fetchData(options);
}, [options]);
// ❌ 不需要使用useMemo的场景:
// 1. 简单计算
const sum = a + b; // 不需要useMemo
// 2. 组件内的临时变量
const isEven = count % 2 === 0; // 不需要useMemo
// 3. 每次都需要新值的情况
const timestamp = Date.now(); // 不需要useMemo9.2 正确使用依赖数组
jsx
// ✅ 好的做法
function GoodPractice() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// 只依赖使用的属性
const greeting = useMemo(() => {
return `Hello, ${user.name}`;
}, [user.name]); // 只依赖name
return <div>{greeting}</div>;
}
// ❌ 避免
function BadPractice() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// 依赖整个对象
const greeting = useMemo(() => {
return `Hello, ${user.name}`;
}, [user]); // user每次都变
return <div>{greeting}</div>;
}9.3 避免副作用
jsx
// ✅ 正确:useMemo只用于计算
function Correct() {
const value = useMemo(() => {
return expensiveCalculation();
}, []);
useEffect(() => {
// 副作用放在useEffect中
updateCache(value);
}, [value]);
return <div>{value}</div>;
}
// ❌ 错误:在useMemo中执行副作用
function Wrong() {
const value = useMemo(() => {
const result = expensiveCalculation();
updateCache(result); // 副作用!
return result;
}, []);
return <div>{value}</div>;
}注意事项
1. 不要过度使用useMemo
jsx
// ❌ 过度优化:简单计算不需要useMemo
function OverOptimized({ a, b }) {
const sum = useMemo(() => a + b, [a, b]); // 不必要
const doubled = useMemo(() => a * 2, [a]); // 不必要
return <div>{sum} - {doubled}</div>;
}
// ✅ 正确:简单计算直接执行
function Correct({ a, b }) {
const sum = a + b;
const doubled = a * 2;
return <div>{sum} - {doubled}</div>;
}
// ✅ 只有复杂计算才使用useMemo
function Correct({ data }) {
const processedData = useMemo(() => {
// 复杂的数据处理逻辑
return data
.filter(item => item.active)
.map(item => ({ ...item, computed: heavyComputation(item) }))
.sort((a, b) => b.priority - a.priority);
}, [data]);
return <DataList data={processedData} />;
}2. useMemo不保证一定缓存
React可能会在某些情况下清除缓存,例如内存压力大时:
jsx
function Component() {
const expensiveValue = useMemo(() => {
console.log('计算中...');
return heavyComputation();
}, []);
// React可能会清除缓存,导致重新计算
// 不要依赖useMemo来保存必须持久化的数据
return <div>{expensiveValue}</div>;
}
// ✅ 如果需要持久化,使用useRef
function ComponentWithRef() {
const valueRef = useRef();
if (!valueRef.current) {
valueRef.current = heavyComputation();
}
return <div>{valueRef.current}</div>;
}3. 依赖数组必须完整
jsx
// ❌ 错误:缺少依赖
function Wrong({ userId, companyId }) {
const userData = useMemo(() => {
return fetchUserData(userId, companyId);
}, [userId]); // 缺少companyId
return <div>{userData.name}</div>;
}
// ✅ 正确:包含所有依赖
function Correct({ userId, companyId }) {
const userData = useMemo(() => {
return fetchUserData(userId, companyId);
}, [userId, companyId]);
return <div>{userData.name}</div>;
}4. 避免在useMemo中执行副作用
jsx
// ❌ 错误:副作用应该在useEffect中
function Wrong({ data }) {
const processed = useMemo(() => {
const result = processData(data);
// 副作用!
localStorage.setItem('cache', JSON.stringify(result));
sendAnalytics('data_processed');
return result;
}, [data]);
return <div>{processed}</div>;
}
// ✅ 正确:分离计算和副作用
function Correct({ data }) {
const processed = useMemo(() => {
return processData(data);
}, [data]);
useEffect(() => {
localStorage.setItem('cache', JSON.stringify(processed));
sendAnalytics('data_processed');
}, [processed]);
return <div>{processed}</div>;
}5. 对象和数组依赖需要特别注意
jsx
// ❌ 问题:对象每次都是新的
function Parent() {
const config = { theme: 'dark', lang: 'zh' }; // 每次渲染都是新对象
return <Child config={config} />;
}
function Child({ config }) {
const settings = useMemo(() => {
return processConfig(config);
}, [config]); // config每次都变,useMemo失效
return <div>{settings.display}</div>;
}
// ✅ 解决方案1:提取到组件外
const CONFIG = { theme: 'dark', lang: 'zh' };
function Parent() {
return <Child config={CONFIG} />;
}
// ✅ 解决方案2:在父组件中useMemo
function Parent() {
const config = useMemo(() => ({ theme: 'dark', lang: 'zh' }), []);
return <Child config={config} />;
}
// ✅ 解决方案3:只依赖具体的值
function Child({ config }) {
const settings = useMemo(() => {
return processConfig(config);
}, [config.theme, config.lang]); // 依赖具体属性
return <div>{settings.display}</div>;
}常见问题
Q1: useMemo和useCallback有什么区别?
A:
useMemo缓存计算结果(值)useCallback缓存函数引用
jsx
// useMemo返回计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// useCallback返回函数本身
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
// useCallback等价于:
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);Q2: 什么时候应该使用useMemo?
A: 在以下情况下使用useMemo:
- 计算开销大:复杂的数据转换、排序、过滤等
- 引用相等性重要:作为其他Hook的依赖,或传给React.memo包裹的组件
- 频繁重新渲染:父组件频繁更新,但计算依赖很少变化
jsx
// ✅ 适合使用useMemo的场景
function DataTable({ data, filters }) {
// 场景1:复杂计算
const processedData = useMemo(() => {
return data
.filter(item => matchesFilters(item, filters))
.map(item => enrichData(item))
.sort((a, b) => a.priority - b.priority);
}, [data, filters]);
// 场景2:作为依赖传递
const config = useMemo(() => ({ sort: 'asc', limit: 10 }), []);
useEffect(() => {
fetchData(config);
}, [config]); // config引用稳定
// 场景3:传给React.memo组件
return <MemoizedChild data={processedData} />;
}Q3: useMemo的依赖数组应该包含什么?
A: 包含useMemo回调函数中使用的所有外部变量:
jsx
function Component() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
const c = 3;
// ✅ 正确:包含所有使用的变量
const result = useMemo(() => {
return a + b + c;
}, [a, b, c]);
// ❌ 错误:缺少依赖
const wrong = useMemo(() => {
return a + b + c;
}, [a]); // 缺少b和c
// ✅ 函数也需要作为依赖
const multiply = (x, y) => x * y;
const product = useMemo(() => {
return multiply(a, b);
}, [a, b, multiply]);
return <div>{result}</div>;
}Q4: useMemo可以用来做数据缓存吗?
A: 不建议。useMemo是性能优化工具,不是数据缓存方案。React可能会清除缓存。
jsx
// ❌ 不可靠的缓存
function Wrong({ userId }) {
const userData = useMemo(() => {
return fetchUserData(userId); // React可能清除缓存
}, [userId]);
return <div>{userData.name}</div>;
}
// ✅ 使用专门的缓存方案
import { useQuery } from 'react-query';
function Correct({ userId }) {
const { data: userData } = useQuery(['user', userId], () => fetchUserData(userId));
return <div>{userData?.name}</div>;
}
// ✅ 或使用状态管理
function AlsoCorrect({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setUserData);
}, [userId]);
return <div>{userData?.name}</div>;
}Q5: 为什么useMemo有时候好像没起作用?
A: 常见原因:
- 依赖项总是变化
jsx
// ❌ 问题:items每次都是新数组
function Parent() {
return <Child items={[1, 2, 3]} />;
}
function Child({ items }) {
const sum = useMemo(() => {
return items.reduce((a, b) => a + b, 0);
}, [items]); // items每次都不同
}
// ✅ 解决:稳定引用
const ITEMS = [1, 2, 3];
function Parent() {
return <Child items={ITEMS} />;
}- 计算本身不昂贵
jsx
// useMemo本身也有开销,简单计算反而更慢
const doubled = useMemo(() => count * 2, [count]); // 不必要- 忘记配合React.memo
jsx
// ❌ 子组件没有memo,useMemo无效
const data = useMemo(() => processData(raw), [raw]);
return <Child data={data} />; // Child每次都重新渲染
// ✅ 配合React.memo
const MemoChild = React.memo(Child);
return <MemoChild data={data} />;Q6: useMemo和计算属性(如Vue的computed)有什么区别?
A: 主要区别:
缓存策略:
- Vue computed:依赖变化时重新计算
- useMemo:依赖变化时重新计算,但React可能清除缓存
使用方式:
- Vue computed:响应式自动追踪依赖
- useMemo:手动声明依赖数组
jsx
// React useMemo
function Component({ data }) {
const filtered = useMemo(() => {
return data.filter(item => item.active);
}, [data]); // 手动声明依赖
return <div>{filtered.length}</div>;
}js
// Vue computed
export default {
data() {
return { data: [] }
},
computed: {
filtered() {
return this.data.filter(item => item.active);
// 自动追踪依赖
}
}
}Q7: React 19的编译器会自动添加useMemo吗?
A: 是的,React 19编译器(React Compiler)会自动优化:
jsx
// 你写的代码
function Component({ items }) {
const filtered = items.filter(item => item.active);
return <div>{filtered.length}</div>;
}
// 编译器自动转换为类似这样的代码
function Component({ items }) {
const filtered = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
return <div>{filtered.length}</div>;
}但是:
- 编译器还在实验阶段
- 不能处理所有场景
- 明确的性能瓶颈仍需手动优化
总结
核心要点
useMemo的作用
- 缓存计算结果,避免重复计算
- 保持引用相等性,减少不必要的渲染
- 是性能优化工具,不是数据缓存方案
使用场景
- ✅ 复杂计算:数据转换、排序、过滤、聚合
- ✅ 引用相等:作为Hook依赖或传给memo组件
- ✅ 频繁渲染:父组件频繁更新,但依赖很少变
- ❌ 简单计算:基本算术、字符串拼接
- ❌ 首次渲染优化:useMemo不能加速首次渲染
性能权衡
jsx// useMemo本身有成本 - 创建函数闭包 - 维护依赖数组 - 比较依赖是否变化 // 只有当计算成本 > useMemo成本时才值得使用依赖数组规则
- 包含所有外部变量
- 使用ESLint插件检查
- 对象/数组依赖需要稳定引用
常见陷阱
- 过度优化导致代码复杂
- 依赖项不稳定导致失效
- 在useMemo中执行副作用
- 依赖useMemo做持久化缓存
最佳实践清单
jsx
// ✅ DO - 推荐做法
1. 只优化明确的性能瓶颈
2. 使用Profiler测量优化效果
3. 配合React.memo和useCallback使用
4. 保持依赖数组的稳定性
5. 先写可读的代码,再优化性能
// ❌ DON'T - 避免做法
1. 不要对所有计算都使用useMemo
2. 不要在useMemo中执行副作用
3. 不要依赖useMemo做数据持久化
4. 不要忽略ESLint的依赖警告
5. 不要过早优化性能优化金字塔
少数关键性能瓶颈
(手动优化)
↑
React.memo + useMemo
(选择性优化)
↑
合理的组件设计
(避免不必要的渲染)
↑
===========================
React 19编译器
(自动优化)学习建议
- 理论基础:深入理解React的渲染机制
- 性能测量:掌握React DevTools Profiler
- 实践经验:在真实项目中应用和调优
- 持续学习:关注React 19编译器的发展
下一步学习
- 学习
useCallback- 函数缓存 - 学习
React.memo- 组件缓存 - 掌握性能优化的完整策略
- 了解React 19的自动优化特性
通过本章学习,你已经全面掌握了useMemo的使用。记住:先写可读的代码,然后根据实际性能问题进行针对性优化。useMemo是强大的工具,但不是银弹,合理使用才能发挥最大价值!