本文介绍React Hook中的useMemo
useMemo 详细使用报告
一、它是什么,解决什么问题
useMemo 是 React 内置 Hook,用于缓存一次计算的结果,只有当依赖项变化时才重新计算。
先看没有 useMemo 的问题
每次组件重渲染,函数体内的所有代码都会重新执行:
function ProductList({ products, filterText }) {
// 每次渲染都重新执行 filter,即使 products 和 filterText 根本没变
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
)
return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
如果父组件因为其他原因重渲染(比如修改了一个跟这个组件完全无关的 state),filteredProducts 也会被重新计算,即使 products 和 filterText 都没变。
useMemo 如何解决
function ProductList({ products, filterText }) {
const filteredProducts = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
),
[products, filterText] // 只有这两个变化时才重新计算
)
return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
二、基本语法
const cachedValue = useMemo(
() => computeExpensiveValue(), // 计算函数(返回你想缓存的值)
[dep1, dep2, ...] // 依赖项数组
)
- 第一个参数:一个函数,返回你想缓存的值,不接受参数
- 第二个参数:依赖项数组,React 用
Object.is(类似===)比较每一项 - 返回值:缓存的结果,依赖不变时每次拿到的是同一个引用
三、执行流程
组件渲染
↓
检查依赖项是否变化(与上次比较)
↓
┌─────────────────────┬──────────────────────────────┐
│ 依赖没变 │ 依赖变了 │
│ 直接返回缓存值 │ 重新执行计算函数 │
│ (同一个引用)✅ │ 缓存新结果并返回 │
└─────────────────────┴──────────────────────────────┘
四、三大核心使用场景
场景一:缓存昂贵的计算结果
适用于计算量较大、输入不频繁变化的场景:
function DataTable({ rawData, sortKey, order }) {
// 排序是 O(n log n),数据量大时有明显开销
const sortedData = useMemo(() => {
console.log('重新排序...') // 可以观察触发频率
return [...rawData].sort((a, b) =>
order === 'asc'
? a[sortKey] > b[sortKey] ? 1 : -1
: a[sortKey] < b[sortKey] ? 1 : -1
)
}, [rawData, sortKey, order]) // 只有三者之一变化才重新排序
return <Table data={sortedData} />
}
场景二:稳定对象/数组引用,避免子组件不必要的重渲染
这是 useMemo 最常见的用途。即使值相同,每次渲染都创建新的对象/数组引用,会导致接收它的子组件认为 props 变了而重渲染:
function ParentComponent({ userId }) {
// ❌ 没有 useMemo:每次渲染都创建新对象,子组件每次都重渲染
const config = { userId, theme: 'dark', pageSize: 10 }
// ✅ 用 useMemo:只有 userId 变化时才创建新对象
const config = useMemo(
() => ({ userId, theme: 'dark', pageSize: 10 }),
[userId]
)
return <ChildComponent config={config} />
}
注意:这通常需要配合
React.memo包裹子组件,否则即使 props 引用没变,子组件也会随父组件重渲染。
场景三:createSelector 工厂函数(与 Redux 配合)
这是在 Redux 文档中提到的重要场景——当同一个带参数的 selector 被多个组件以不同参数调用时,createSelector 的默认缓存(只缓存最后一次)会失效。解决方案是用工厂函数 + useMemo:
import { useMemo } from 'react'
import { createSelector } from '@reduxjs/toolkit'
// 工厂函数:每次调用都创建一个 selector 实例
const makeSelectPostsByUser = () => createSelector(
state => Object.values(state.posts.entities),
(state, userId) => userId,
(posts, userId) => posts.filter(post => post.user === userId)
)
function UserPostsList({ userId }) {
// useMemo 确保每个组件实例只创建一次 selector,而不是每次渲染都创建
const selectPostsByUser = useMemo(makeSelectPostsByUser, [])
// ↑ 空依赖数组
// = 只在挂载时创建一次
const posts = useSelector(state => selectPostsByUser(state, userId))
return <PostList posts={posts} />
}
// 页面上同时有两个实例时,每个实例都有独立的缓存,互不干扰:
// <UserPostsList userId="user-1" /> → 独立缓存,缓存 user-1 的帖子
// <UserPostsList userId="user-2" /> → 独立缓存,缓存 user-2 的帖子
如果不用 useMemo,makeSelectPostsByUser() 在组件每次渲染时都会被调用,产生一个新的 selector 实例,上次的缓存全部丢失。
五、依赖项的写法规则
依赖项应该包含计算函数内部用到的所有外部变量
// 计算函数用到了 list 和 keyword,两者都要写进依赖项
const result = useMemo(
() => list.filter(item => item.name.includes(keyword)),
[list, keyword] // ✅ 完整
)
依赖项为空数组:只在挂载时执行一次
// 只在组件首次挂载时计算,之后永远用缓存值
const instance = useMemo(() => new HeavyClass(), [])
基本类型 vs 引用类型
const [count, setCount] = useState(0)
const [name, setName] = useState('Alice')
// count 是数字(基本类型),只要值相同,=== 就相同,可以正常触发缓存判断
const double = useMemo(() => count * 2, [count])
// 如果依赖项是对象,注意引用稳定性
const options = { page: 1 } // ⚠️ 每次渲染都是新引用
const result = useMemo(() => fetchData(options), [options])
// 上面的 options 每次渲染都变,useMemo 永远无法命中缓存,等于没用
六、useMemo vs useCallback
两者非常相似,区别在于缓存的东西不同:
useMemo | useCallback | |
|---|---|---|
| 缓存什么 | 函数的返回值(任意类型) | 函数本身(函数引用) |
| 语法 | useMemo(() => value, deps) | useCallback(fn, deps) |
| 典型用途 | 缓存计算结果、对象、数组 | 缓存事件处理函数,传给子组件 |
useCallback(fn, deps) 其实就是 useMemo(() => fn, deps) 的语法糖。
// 这两个写法完全等价:
const handleClick = useCallback(() => doSomething(id), [id])
const handleClick = useMemo(() => () => doSomething(id), [id])
七、什么时候不该用 useMemo
useMemo 本身也有成本(需要存储缓存、比较依赖项),滥用反而拖慢性能。
❌ 不该用:计算本身非常简单
// 这种计算极快,useMemo 的开销反而更大
const double = useMemo(() => count * 2, [count])
// 直接写就好:
const double = count * 2
❌ 不该用:依赖项每次都变
function Component() {
const obj = { x: 1 } // 每次渲染新引用
// obj 每次引用都变,useMemo 每次都重新计算,完全没有缓存效果
const result = useMemo(() => heavyCompute(obj), [obj])
}
✅ 该用的情况
- 计算明显耗时(大数组排序/过滤、复杂数学运算)
- 需要稳定对象/数组引用传给使用
React.memo的子组件 - 配合
createSelector工厂函数 - 依赖项变化频率远低于组件重渲染频率
八、如何判断是否需要 useMemo
可以用 React DevTools Profiler 或在计算函数中打印来测量:
const sortedData = useMemo(() => {
const start = performance.now()
const result = [...data].sort(...)
console.log(`排序耗时: ${performance.now() - start}ms`)
return result
}, [data])
一般来说,耗时超过 1ms 的计算才值得使用 useMemo。
九、总结
需不需要 useMemo?
计算结果是否昂贵?
是 → 用 useMemo 缓存计算结果
返回的是对象/数组,且要传给子组件?
是,且子组件用了 React.memo → 用 useMemo 稳定引用
在 Redux 中用 createSelector 带参数时,同一个 selector 被多个组件实例使用?
是 → 工厂函数 + useMemo 创建独立 selector 实例
以上都不是?
不需要 useMemo,直接计算就好