本文介绍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 也会被重新计算,即使 productsfilterText 都没变。

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 的帖子

如果不用 useMemomakeSelectPostsByUser() 在组件每次渲染时都会被调用,产生一个新的 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

两者非常相似,区别在于缓存的东西不同:

useMemouseCallback
缓存什么函数的返回值(任意类型)函数本身(函数引用)
语法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,直接计算就好