4、useMemo:计算结果的记忆化、依赖数组的正确使用、避免过度优化
好,咱们来聊聊 useMemo。这个 Hook 在 React 性能优化里,属于那种「用好了是神器,用错了是累赘」的角色。我个人习惯把它看作一个「缓存计算结果的工具」,而不是什么万能药。
4.1 什么是 useMemo?它解决了什么问题?
先问一个问题:你写过一个组件,里面有个复杂的计算逻辑,比如过滤一个很大的列表,或者对数据进行排序。每次组件重新渲染,这个计算都要重新跑一遍。哪怕 props 根本没变,它也得跑。
这就是 useMemo 要解决的问题。它能把计算结果「记住」,只有当依赖项发生变化时,才重新计算。说白了,就是避免无意义的重复劳动。
核心思想:用空间换时间。把计算结果缓存起来,下次直接取缓存,不用再算一遍。
import { useMemo } from 'react';
function ExpensiveList({ items, filter }) {
// 这个计算很重,每次渲染都跑一遍太亏了
const filteredItems = useMemo(() => {
console.log('开始过滤...');
return items.filter(item => item.name.includes(filter));
}, [items, filter]); // 只有 items 或 filter 变了才重新计算
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
你看,这个例子里的过滤操作,如果 items 有几千条,每次渲染都跑一遍,性能肯定扛不住。用 useMemo 包一下,只有 items 或 filter 变了才重新算,其他时候直接拿缓存结果。
4.2 依赖数组的正确使用
依赖数组是 useMemo 的灵魂。用错了,要么缓存失效,要么出现 bug。我在项目中遇到过好几次,同事把依赖写漏了,结果页面数据死活不更新。
常见错误:依赖数组里漏掉某个变量,或者把不必要的变量加进去。
依赖数组的规则其实很简单:你在回调函数里用到的所有响应式值,都必须列在依赖数组里。响应式值包括 props、state、以及从其他 Hook 返回的值。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ❌ 错误:依赖数组为空,但回调里用了 userId
const displayName = useMemo(() => {
return user ? `${user.name} (ID: ${userId})` : '加载中...';
}, []); // 应该加上 userId 和 user
// ✅ 正确:把所有用到的响应式值都列出来
const displayName = useMemo(() => {
return user ? `${user.name} (ID: ${userId})` : '加载中...';
}, [user, userId]);
}
嗯,这里要注意:不要依赖数组里啥都不写。空数组意味着「永远不重新计算」,这在某些场景下确实有用(比如初始化一次),但大多数情况下会导致 bug。
反过来,也别把依赖写得太满。比如一个函数里用了很多变量,但其中有些是稳定的(比如 setState 函数),就没必要加进去。React 官方文档里说过,setState 函数是稳定的,不会变。
小技巧:如果你不确定该加哪些依赖,可以用 ESLint 的 react-hooks/exhaustive-deps 规则。它会自动帮你检查,省心不少。
4.3 避免过度优化
这是 useMemo 最容易被滥用的地方。很多开发者一上来就给所有计算都套上 useMemo,觉得「反正加了也没坏处」。其实不然。
为什么?因为 useMemo 本身也有开销。它需要创建闭包、比较依赖数组、管理缓存。如果计算本身很简单,比如一个加法、一个字符串拼接,那 useMemo 的开销可能比计算本身还大。
// ❌ 过度优化:一个简单的加法,根本不需要 useMemo
const total = useMemo(() => a + b, [a, b]);
// ✅ 直接计算,更简单,性能反而更好
const total = a + b;
我曾经见过一个项目,几乎每个组件里都有 useMemo,连 const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName]) 这种都写。结果代码变得又长又难读,性能反而没提升。
那什么时候该用 useMemo?我个人习惯遵循这几个原则:
- 计算开销确实大:比如遍历大数组、复杂数学运算、数据转换等。如果计算时间超过 1ms,可以考虑用。
- 计算结果被多次使用:比如同一个计算结果传给多个子组件,或者作为其他 Hook 的依赖。
- 避免不必要的子组件重渲染:当 useMemo 返回的对象或数组作为 props 传给子组件时,可以配合 React.memo 使用,避免子组件无意义重渲染。
一句话总结:先测量,再优化。别凭感觉加 useMemo,用 Profiler 看看实际性能瓶颈在哪。
4.4 useMemo 与 useCallback 的区别
很多新手会搞混这两个 Hook。简单来说:
- useMemo:缓存计算结果,返回的是值。
- useCallback:缓存函数本身,返回的是函数。
其实 useCallback 就是 useMemo 的一个特例。你可以把 useCallback(fn, deps) 理解为 useMemo(() => fn, deps)。但为了语义清晰,React 专门提供了 useCallback。
// 这两个是等价的
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const handleClick = useMemo(() => () => {
doSomething(a, b);
}, [a, b]);
我个人习惯:缓存值用 useMemo,缓存函数用 useCallback。这样代码读起来更直观。
4.5 实战中的避坑指南
最后分享几个我踩过的坑:
坑一:useMemo 里不要有副作用。useMemo 是纯计算用的,如果你在里面发了请求、改了 state,那就是滥用。副作用请用 useEffect。
坑二:依赖数组里不要用对象或数组字面量。比如 useMemo(() => {...}, [{ a: 1 }]),每次渲染都会创建一个新对象,导致依赖永远在变,缓存失效。
// ❌ 错误:每次渲染都创建新对象,依赖永远在变
const result = useMemo(() => {
return expensiveCalculation(data);
}, [{ data }]); // 这里 { data } 每次都是新对象
// ✅ 正确:直接用 data 本身
const result = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
坑三:不要用 useMemo 来「防止」组件重渲染。useMemo 只缓存值,不缓存组件。防止重渲染要用 React.memo。
嗯,差不多就这些。useMemo 是个好工具,但别把它当锤子,看啥都像钉子。记住:性能优化的第一步,永远是测量。没有数据支撑的优化,都是耍流氓。