useMemo 是一个 React Hook,它可以让你在重新渲染之间缓存计算的结果。

const cachedValue = useMemo(calculateValue, dependencies)

注意

React 编译器 会自动缓存值和函数,从而减少手动 useMemo 调用的需求。你可以使用编译器自动处理缓存。


参考

🌐 Reference

useMemo(calculateValue, dependencies)

在组件的顶层调用 useMemo 来在重新渲染之间缓存计算结果:

🌐 Call useMemo at the top level of your component to cache a calculation between re-renders:

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}

查看更多示例。

参数

🌐 Parameters

  • calculateValue:计算你想要缓存的值的函数。它应该是纯函数,不应接收任何参数,并且应返回任何类型的值。React 会在初始渲染期间调用你的函数。在随后的渲染中,如果自上次渲染以来 dependencies 没有改变,React 会再次返回相同的值。否则,它将调用 calculateValue,返回其结果,并将其存储以便以后重用。
  • dependenciescalculateValue 代码中引用的所有响应式值的列表。响应式值包括 props、state,以及在组件主体中直接声明的所有变量和函数。如果你的代码检查器为 React 配置,它将验证每个响应式值是否被正确指定为依赖。依赖列表必须有一个固定数量的项目,并且必须像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较每个依赖与其先前的值。

返回

🌐 Returns

在初次渲染时,useMemo 返回调用 calculateValue 无参数时的结果。

🌐 On the initial render, useMemo returns the result of calling calculateValue with no arguments.

在接下来的渲染中,它要么返回上一次渲染中已存储的值(如果依赖没有变化),要么再次调用 calculateValue,并返回 calculateValue 返回的结果。

🌐 During next renders, it will either return an already stored value from the last render (if the dependencies haven’t changed), or call calculateValue again, and return the result that calculateValue has returned.

注意事项

🌐 Caveats

  • useMemo 是一个 Hook,因此你只能在你的组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件并将状态移入其中。
  • 在严格模式下,React 会调用你的计算函数两次,以便帮助你发现意外的副作用。这是仅在开发环境下的行为,对生产环境没有影响。如果你的计算函数是纯函数(它应该是纯函数),这不会影响你的逻辑。其中一次调用的结果将被忽略。
  • React 不会丢弃缓存的值,除非有特定的原因这么做。 例如,在开发环境中,当你编辑组件文件时,React 会丢弃缓存。在开发环境和生产环境中,如果你的组件在初次挂载时挂起,React 也会丢弃缓存。未来,React 可能会增加更多利用丢弃缓存的功能——例如,如果未来 React 对虚拟列表提供内置支持,那么丢弃滚出虚拟表视口的项目的缓存是有意义的。如果你仅仅依赖 useMemo 作为性能优化,这应该没问题。否则,状态变量ref 可能更合适。

注意

像这样缓存返回值也被称为记忆化,这就是为什么这个 Hook 被称为 useMemo

🌐 Caching return values like this is also known as memoization, which is why this Hook is called useMemo.


用法

🌐 Usage

跳过昂贵的重新计算

🌐 Skipping expensive recalculations

为了在重新渲染之间缓存计算结果,请在组件的顶层将其封装在 useMemo 调用中:

🌐 To cache a calculation between re-renders, wrap it in a useMemo call at the top level of your component:

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}

你需要传递两样东西给 useMemo

🌐 You need to pass two things to useMemo:

  1. 一个不带参数的 计算函数 ,像 () =>,并返回你想要计算的结果。
  2. 一个依赖列表,包括你组件中用于计算的每一个值。

在初始渲染时,你从 useMemo 获取的 将是调用你的 计算的结果。

在每次后续渲染时,React 会将 依赖 与你在上一次渲染中传递的依赖进行比较。如果没有任何依赖发生变化(与 Object.is 相比),useMemo 将返回你之前计算的值。否则,React 会重新运行你的计算并返回新值。

换句话说,useMemo 会在重新渲染之间缓存计算结果,直到其依赖发生变化。

🌐 In other words, useMemo caches a calculation result between re-renders until its dependencies change.

让我们通过一个例子来看看这什么时候有用。

默认情况下,每次 React 重新渲染时,都会重新运行组件的整个主体。例如,如果这个 TodoList 更新了它的状态或从其父组件接收到新的 props,filterTodos 函数将会重新运行:

🌐 By default, React will re-run the entire body of your component every time that it re-renders. For example, if this TodoList updates its state or receives new props from its parent, the filterTodos function will re-run:

function TodoList({ todos, tab, theme }) {
const visibleTodos = filterTodos(todos, tab);
// ...
}

通常,这不是问题,因为大多数计算都非常快。然而,如果你在过滤或转换一个大型数组,或者进行一些耗资源的计算,你可能希望在数据没有变化的情况下跳过再次计算。如果 todostab 与上一次渲染时相同,那么像之前那样将计算封装在 useMemo 中可以让你重用之前已经计算过的 visibleTodos

🌐 Usually, this isn’t a problem because most calculations are very fast. However, if you’re filtering or transforming a large array, or doing some expensive computation, you might want to skip doing it again if data hasn’t changed. If both todos and tab are the same as they were during the last render, wrapping the calculation in useMemo like earlier lets you reuse visibleTodos you’ve already calculated before.

这种类型的缓存被称为 记忆化

🌐 This type of caching is called memoization.

注意

你应该只将 useMemo 作为性能优化来依赖。 如果你的代码在没有它的情况下无法运行,先找到潜在问题并修复它。然后你可以添加 useMemo 来提高性能。

深入研究

如何判断计算是否昂贵?

🌐 How to tell if a calculation is expensive?

一般来说,除非你在创建或循环数千个对象,否则它可能并不昂贵。如果你想更有信心,你可以添加一个控制台日志来测量某段代码花费的时间:

🌐 In general, unless you’re creating or looping over thousands of objects, it’s probably not expensive. If you want to get more confidence, you can add a console log to measure the time spent in a piece of code:

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

执行你正在测量的交互(例如,在输入框中输入)。然后你将在控制台中看到类似 filter array: 0.15ms 的日志。如果总体记录的时间累计达到一个显著的数值(比如 1ms 或更多),那么记忆该计算可能是有意义的。作为实验,你可以将该计算封装在 useMemo 中,以验证该交互的总记录时间是否有所减少:

🌐 Perform the interaction you’re measuring (for example, typing into the input). You will then see logs like filter array: 0.15ms in your console. If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation. As an experiment, you can then wrap the calculation in useMemo to verify whether the total logged time has decreased for that interaction or not:

console.time('filter array');
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab); // Skipped if todos and tab haven't changed
}, [todos, tab]);
console.timeEnd('filter array');

useMemo 不会让第一次渲染更快。它只会帮助你在更新时跳过不必要的工作。

请记住,你的机器可能比用户的机器更快,因此用人为减速来测试性能是一个好主意。例如,Chrome 提供了一个 CPU 限速 选项来实现这一点。

🌐 Keep in mind that your machine is probably faster than your users’ so it’s a good idea to test the performance with an artificial slowdown. For example, Chrome offers a CPU Throttling option for this.

还要注意,在开发中测量性能不会给你最准确的结果。(例如,当严格模式开启时,你会看到每个组件渲染两次而不是一次。)要获得最准确的时间,应该将你的应用构建为生产环境版本,并在像用户一样的设备上进行测试。

🌐 Also note that measuring performance in development will not give you the most accurate results. (For example, when Strict Mode is on, you will see each component render twice rather than once.) To get the most accurate timings, build your app for production and test it on a device like your users have.

深入研究

你应该在所有地方添加 useMemo 吗?

🌐 Should you add useMemo everywhere?

如果你的应用像这个网站一样,并且大多数交互是粗略的(比如替换一个页面或整个部分),通常不需要记忆化。另一方面,如果你的应用更像一个绘图编辑器,并且大多数交互是细粒度的(比如移动形状),那么你可能会发现记忆化非常有用。

🌐 If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.

使用 useMemo 进行优化只有在少数情况下才有价值:

🌐 Optimizing with useMemo is only valuable in a few cases:

  • 你在 useMemo 中进行的计算明显很慢,而且它的依赖几乎不变化。
  • 你将它作为属性传递给一个被 memo. 封装的组件。如果值没有变化,你希望跳过重新渲染。记忆化让你的组件只有在依赖不同时才重新渲染。
  • 你传递的值随后被用作某些 Hook 的依赖。例如,也许另一个 useMemo 计算值依赖于它。或者你可能依赖这个来自 useEffect. 的值

在其他情况下,将计算封装在 useMemo 中没有任何好处。这么做也没有显著的坏处,所以有些团队选择不去考虑具体的个别情况,而是尽可能进行记忆化。这种方法的缺点是代码的可读性会降低。此外,并非所有的记忆化都是有效的:一个“总是新的”单一值就足以破坏整个组件的记忆化效果。

🌐 There is no benefit to wrapping a calculation in useMemo in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside of this approach is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.

在实践中,通过遵循几个原则,你可以使大量的 memo 化变得不必要:

  1. 当一个组件在视觉上封装其他组件时,让它接受 JSX 作为子组件。 这样,当封装组件更新自身状态时,React 就知道其子组件不需要重新渲染。
  2. 优先使用本地状态,并且不要将状态提升得比必要的更高。例如,不要将像表单或某个项目是否被悬停这样的临时状态保存在树的顶端或全局状态库中。
  3. 保持你的渲染逻辑纯粹。 如果重新渲染一个组件导致问题或产生一些明显的视觉瑕疵,那就是你的组件中的一个错误!修复这个错误,而不是添加记忆化。
  4. 避免不必要的会更新状态的效果。 大多数 React 应用的性能问题都是由效果引起的一系列更新造成的,这些更新会导致组件不断重新渲染。
  5. 尝试从你的 Effect 中移除不必要的依赖。 例如,与其使用记忆化,通常更简单的方法是将某些对象或函数移动到 Effect 内部或组件外部。

如果某个具体的交互仍然感觉有延迟,使用 React 开发者工具的 profiler 来查看哪些组件最能从 memoization 中受益,并在需要的地方添加 memoization。这些原则使你的组件更容易调试和理解,所以无论如何遵循它们都是好的。从长远来看,我们正在研究自动进行细粒度的 memoization,以一次性彻底解决这个问题。

🌐 If a specific interaction still feels laggy, use the React Developer Tools profiler to see which components would benefit the most from memoization, and add memoization where needed. These principles make your components easier to debug and understand, so it’s good to follow them in any case. In the long term, we’re researching doing granular memoization automatically to solve this once and for all.

The difference between useMemo and calculating a value directly

例子 1 of 2:
跳过使用 useMemo 的重新计算

🌐 Skipping recalculation with useMemo

在这个例子中,filterTodos 实现被人为减慢,以便你可以看到在渲染过程中调用的一些 JavaScript 函数真正变慢时会发生什么。尝试切换标签并切换主题。

🌐 In this example, the filterTodos implementation is artificially slowed down so that you can see what happens when some JavaScript function you’re calling during rendering is genuinely slow. Try switching the tabs and toggling the theme.

切换标签感觉很慢,因为它迫使被减速的 filterTodos 重新执行。这是预期的,因为 tab 已经更改,因此整个计算 需要 重新运行。(如果你想知道为什么它会运行两次,可以在 这里 查看解释。)

🌐 Switching the tabs feels slow because it forces the slowed down filterTodos to re-execute. That’s expected because the tab has changed, and so the entire calculation needs to re-run. (If you’re curious why it runs twice, it’s explained here.)

切换主题。多亏了 useMemo,即使有人工延迟也很快! 慢速的 filterTodos 调用被跳过,因为自上次渲染以来,todostab(你作为依赖传递给 useMemo)都没有变化。

🌐 Toggle the theme. Thanks to useMemo, it’s fast despite the artificial slowdown! The slow filterTodos call was skipped because both todos and tab (which you pass as dependencies to useMemo) haven’t changed since the last render.

import { useMemo } from 'react';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}


跳过组件的重新渲染

🌐 Skipping re-rendering of components

在某些情况下,useMemo 也可以帮助你优化子组件的重新渲染性能。为说明这一点,假设这个 TodoList 组件将 visibleTodos 作为 prop 传递给子组件 List

🌐 In some cases, useMemo can also help you optimize performance of re-rendering child components. To illustrate this, let’s say this TodoList component passes the visibleTodos as a prop to the child List component:

export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}

你已经注意到切换 theme 属性会让应用暂时冻结,但如果你从 JSX 中移除 <List />,应用感觉很快。这说明值得尝试优化 List 组件。

🌐 You’ve noticed that toggling the theme prop freezes the app for a moment, but if you remove <List /> from your JSX, it feels fast. This tells you that it’s worth trying to optimize the List component.

默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件。 这就是为什么当 TodoListtheme 不同的情况下重新渲染时,List 组件也会重新渲染。这对于不需要大量计算的组件来说是可以的。但是,如果你已经确认重新渲染很慢,你可以通过将 List 封装在 memo: 中来告诉它在 props 与上次渲染相同时跳过重新渲染。

import { memo } from 'react';

const List = memo(function List({ items }) {
// ...
});

有了这个改变,如果 List 的所有属性与上一次渲染时相同,它将跳过重新渲染。 这就是为什么缓存计算变得很重要的原因!想象一下,你在没有 useMemo 的情况下计算了 visibleTodos

export default function TodoList({ todos, tab, theme }) {
// Every time the theme changes, this will be a different array...
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>

<List items={visibleTodos} />
</div>
);
}

在上述示例中,filterTodos 函数总是创建一个不同的数组, 类似于 {} 对象字面量总是创建一个新对象。通常,这不会成为问题,但这意味着 List 属性永远不会相同,并且你的 memo 优化将不起作用。这就是 useMemo 派上用场的地方:

export default function TodoList({ todos, tab, theme }) {
// Tell React to cache your calculation between re-renders...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...so as long as these dependencies don't change...
);
return (
<div className={theme}>

<List items={visibleTodos} />
</div>
);
}

通过将 visibleTodos 计算封装在 useMemo 中,你可以确保它在重新渲染之间具有 相同 的值(直到依赖发生变化)。除非出于某些特定原因,否则你不 必需 将计算封装在 useMemo 中。在这个例子中,原因是你将它传递给一个通过 memo, 封装的组件,这样可以让组件跳过重新渲染。还有一些添加 useMemo 的其他原因,这些原因在本页的后面有描述。

深入研究

记忆化单个 JSX 节点

🌐 Memoizing individual JSX nodes

你可以把 <List /> JSX 节点本身封装在 useMemo 中,而不是将 List 封装在 memo 中:

🌐 Instead of wrapping List in memo, you could wrap the <List /> JSX node itself in useMemo:

export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{children}
</div>
);
}

行为将是相同的。如果 visibleTodos 没有变化,List 不会重新渲染。

🌐 The behavior would be the same. If the visibleTodos haven’t changed, List won’t be re-rendered.

<List items={visibleTodos} /> 这样的 JSX 节点是一个像 { type: List, props: { items: visibleTodos } } 这样的对象。创建这个对象的成本非常低,但 React 并不知道它的内容是否与上一次相同。这就是为什么默认情况下,React 会重新渲染 List 组件。

🌐 A JSX node like <List items={visibleTodos} /> is an object like { type: List, props: { items: visibleTodos } }. Creating this object is very cheap, but React doesn’t know whether its contents is the same as last time or not. This is why by default, React will re-render the List component.

然而,如果 React 看到与上一次渲染完全相同的 JSX,它不会尝试重新渲染你的组件。这是因为 JSX 节点是不可变的。一个 JSX 节点对象不会随时间改变,所以 React 知道可以安全地跳过重新渲染。然而,为了使这起作用,节点必须实际上是同一个对象,而不仅仅是在代码中看起来相同。这就是 useMemo 在这个示例中所做的事情。

🌐 However, if React sees the same exact JSX as during the previous render, it won’t try to re-render your component. This is because JSX nodes are immutable. A JSX node object could not have changed over time, so React knows it’s safe to skip a re-render. However, for this to work, the node has to actually be the same object, not merely look the same in code. This is what useMemo does in this example.

手动将 JSX 节点封装到 useMemo 中并不方便。例如,你不能有条件地执行此操作。这通常也是为什么你会使用 memo 来封装组件,而不是封装 JSX 节点的原因。

🌐 Manually wrapping JSX nodes into useMemo is not convenient. For example, you can’t do this conditionally. This is usually why you would wrap components with memo instead of wrapping JSX nodes.

The difference between skipping re-renders and always re-rendering

例子 1 of 2:
跳过使用 useMemomemo 的重新渲染

🌐 Skipping re-rendering with useMemo and memo

在此示例中,List 组件被人为地减慢,这样你就可以看到当你渲染的 React 组件真正变慢时会发生什么。尝试切换标签并切换主题。

🌐 In this example, the List component is artificially slowed down so that you can see what happens when a React component you’re rendering is genuinely slow. Try switching the tabs and toggling the theme.

切换标签感觉很慢,因为它强制已经变慢的 List 重新渲染。这是预期的,因为 tab 已经发生了变化,因此你需要在屏幕上反映用户的新选择。

🌐 Switching the tabs feels slow because it forces the slowed down List to re-render. That’s expected because the tab has changed, and so you need to reflect the user’s new choice on the screen.

接下来,尝试切换主题。多亏了 useMemomemo,即使有人工延迟,也依然很快! List 跳过了重新渲染,因为 visibleTodos 数组自上次渲染以来没有变化。visibleTodos 数组没有变化,因为 todostab(你作为依赖传递给 useMemo)自上次渲染以来都没有变化。

🌐 Next, try toggling the theme. Thanks to useMemo together with memo, it’s fast despite the artificial slowdown! The List skipped re-rendering because the visibleTodos array has not changed since the last render. The visibleTodos array has not changed because both todos and tab (which you pass as dependencies to useMemo) haven’t changed since the last render.

import { useMemo } from 'react';
import List from './List.js';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>
      <List items={visibleTodos} />
    </div>
  );
}


防止副作用过于频繁地触发

🌐 Preventing an Effect from firing too often

有时,你可能想在 Effect: 中使用一个值

🌐 Sometimes, you might want to use a value inside an Effect:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = {
serverUrl: 'https://localhost:1234',
roomId: roomId
}

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

这会导致一个问题。每个响应式值都必须被声明为你的 Effect 的依赖。 但是,如果你将 options 声明为依赖,它将导致你的 Effect 不断重新连接到聊天室:

🌐 This creates a problem. Every reactive value must be declared as a dependency of your Effect. However, if you declare options as a dependency, it will cause your Effect to constantly reconnect to the chat room:

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 🔴 Problem: This dependency changes on every render
// ...

为了解决这个问题,你可以在 useMemo 中封装你需要从 Effect 调用的对象:

🌐 To solve this, you can wrap the object you need to call from an Effect in useMemo:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = useMemo(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Only changes when options changes
// ...

这确保如果 useMemo 返回缓存的对象,options 对象在重新渲染之间是相同的。

🌐 This ensures that the options object is the same between re-renders if useMemo returns the cached object.

但是,由于 useMemo 是性能优化,而不是语义保证,如果有具体原因,React 可能会丢弃缓存的值。这也会导致 effect 重新触发,因此通过将对象放入 Effect 内,可以更好地消除对函数依赖的需求

🌐 However, since useMemo is performance optimization, not a semantic guarantee, React may throw away the cached value if there is a specific reason to do that. This will also cause the effect to re-fire, so it’s even better to remove the need for a function dependency by moving your object inside the Effect:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
const options = { // ✅ No need for useMemo or object dependencies!
serverUrl: 'https://localhost:1234',
roomId: roomId
}

const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...

现在你的代码更简单,不需要 useMemo了解更多关于移除 Effect 依赖的信息。

🌐 Now your code is simpler and doesn’t need useMemo. Learn more about removing Effect dependencies.

记忆化另一个钩子的依赖

🌐 Memoizing a dependency of another Hook

假设你有一个计算依赖于直接在组件主体中创建的对象:

🌐 Suppose you have a calculation that depends on an object created directly in the component body:

function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };

const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...

依赖像这样的对象会违背记忆化的目的。当一个组件重新渲染时,组件体内的所有代码都会再次运行。**创建 searchOptions 对象的代码行也会在每次重新渲染时运行。**由于 searchOptions 是你 useMemo 调用的依赖,并且每次都不同,React 会知道依赖不同,因此每次都会重新计算 searchItems

🌐 Depending on an object like this defeats the point of memoization. When a component re-renders, all of the code directly inside the component body runs again. The lines of code creating the searchOptions object will also run on every re-render. Since searchOptions is a dependency of your useMemo call, and it’s different every time, React knows the dependencies are different, and recalculate searchItems every time.

要解决这个问题,你可以在将 searchOptions 对象作为依赖传递之前对其 本身 进行记忆化处理:

🌐 To fix this, you could memoize the searchOptions object itself before passing it as a dependency:

function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes

const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...

在上面的例子中,如果 text 没有变化,searchOptions 对象也不会变化。然而,一个更好的修复方法是将 searchOptions 对象声明放到 useMemo 计算函数内部:

🌐 In the example above, if the text did not change, the searchOptions object also won’t change. However, an even better fix is to move the searchOptions object declaration inside of the useMemo calculation function:

function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...

现在你的计算直接依赖于 text(它是一个字符串,不能“意外”变成不同的值)。

🌐 Now your calculation depends on text directly (which is a string and can’t “accidentally” become different).


记忆化一个函数

🌐 Memoizing a function

假设 Form 组件被 memo. 封装。你想将一个函数作为 prop 传给它:

🌐 Suppose the Form component is wrapped in memo. You want to pass a function to it as a prop:

export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}

return <Form onSubmit={handleSubmit} />;
}

正如 {} 会创建一个不同的对象一样,像 function() {} 这样的函数声明和像 () => {} 这样的表达式在每次重新渲染时也会生成一个不同的函数。单独来看,创建一个新函数本身并不是问题。这不是需要避免的事情!然而,如果 Form 组件被进行了 memo 化,通常你希望在属性未改变时跳过重新渲染。一个总是不同的属性会破坏 memo 化的意义。

🌐 Just as {} creates a different object, function declarations like function() {} and expressions like () => {} produce a different function on every re-render. By itself, creating a new function is not a problem. This is not something to avoid! However, if the Form component is memoized, presumably you want to skip re-rendering it when no props have changed. A prop that is always different would defeat the point of memoization.

要使用 useMemo 对一个函数进行记忆化,你的计算函数必须返回另一个函数:

🌐 To memoize a function with useMemo, your calculation function would have to return another function:

export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);

return <Form onSubmit={handleSubmit} />;
}

这看起来很笨重!记忆化函数足够常见,以至于 React 特意提供了一个内置 Hook 来处理这个问题。将你的函数封装到 useCallback 中,而不是 useMemo,以避免必须编写额外的嵌套函数:

🌐 This looks clunky! Memoizing functions is common enough that React has a built-in Hook specifically for that. Wrap your functions into useCallback instead of useMemo to avoid having to write an extra nested function:

export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);

return <Form onSubmit={handleSubmit} />;
}

上面的两个例子完全等价。useCallback 唯一的好处是它可以让你避免在内部编写额外的嵌套函数。它没有其他作用。阅读更多关于 useCallback 的内容。

🌐 The two examples above are completely equivalent. The only benefit to useCallback is that it lets you avoid writing an extra nested function inside. It doesn’t do anything else. Read more about useCallback.


故障排除

🌐 Troubleshooting

我的计算在每次重新渲染时运行两次

🌐 My calculation runs twice on every re-render

严格模式 下,React 会调用你的一些函数两次,而不是一次:

🌐 In Strict Mode, React will call some of your functions twice instead of once:

function TodoList({ todos, tab }) {
// This component function will run twice for every render.

const visibleTodos = useMemo(() => {
// This calculation will run twice if any of the dependencies change.
return filterTodos(todos, tab);
}, [todos, tab]);

// ...

这是预期的,不应破坏你的代码。

🌐 This is expected and shouldn’t break your code.

这种仅在开发模式下的行为可以帮助你保持组件的纯粹性。React 会使用其中一次调用的结果,并忽略另一次调用的结果。只要你的组件和计算函数是纯函数,这不应影响你的逻辑。然而,如果它们意外地不是纯函数,这将帮助你注意到并修复错误。

🌐 This development-only behavior helps you keep components pure. React uses the result of one of the calls, and ignores the result of the other call. As long as your component and calculation functions are pure, this shouldn’t affect your logic. However, if they are accidentally impure, this helps you notice and fix the mistake.

例如,这个不纯的计算函数会改变你作为属性收到的数组:

🌐 For example, this impure calculation function mutates an array you received as a prop:

const visibleTodos = useMemo(() => {
// 🚩 Mistake: mutating a prop
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);

React 会调用你的函数两次,所以你会注意到待办事项被添加了两次。你的计算不应该更改任何现有对象,但可以更改在计算过程中创建的任何对象。例如,如果 filterTodos 函数总是返回一个不同的数组,你可以修改那个数组:

🌐 React calls your function twice, so you’d notice the todo is added twice. Your calculation shouldn’t change any existing objects, but it’s okay to change any new objects you created during the calculation. For example, if the filterTodos function always returns a different array, you can mutate that array instead:

const visibleTodos = useMemo(() => {
const filtered = filterTodos(todos, tab);
// ✅ Correct: mutating an object you created during the calculation
filtered.push({ id: 'last', text: 'Go for a walk!' });
return filtered;
}, [todos, tab]);

阅读 保持组件纯粹 以了解更多关于纯粹的信息。

🌐 Read keeping components pure to learn more about purity.

另外,请查看关于更新对象更新数组而不进行修改的指南。

🌐 Also, check out the guides on updating objects and updating arrays without mutation.


我的 useMemo 调用应该返回一个对象,但返回了未定义

🌐 My useMemo call is supposed to return an object, but returns undefined

此代码不起作用:

🌐 This code doesn’t work:

// 🔴 You can't return an object from an arrow function with () => {
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);

在 JavaScript 中,() => { 开始箭头函数的函数体,因此 { 大括号不是你的对象的一部分。这就是为什么它不返回对象,并且会导致错误。你可以通过添加括号来修复它,如 ({}):

🌐 In JavaScript, () => { starts the arrow function body, so the { brace is not a part of your object. This is why it doesn’t return an object, and leads to mistakes. You could fix it by adding parentheses like ({ and }):

// This works, but is easy for someone to break again
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);

然而,这仍然令人困惑,而且对于某些人来说,通过移除括号来破解它太容易了。

🌐 However, this is still confusing and too easy for someone to break by removing the parentheses.

为了避免这个错误,请明确写出 return 语句:

🌐 To avoid this mistake, write a return statement explicitly:

// ✅ This works and is explicit
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);

每次我的组件渲染时,useMemo 中的计算都会重新运行

🌐 Every time my component renders, the calculation in useMemo re-runs

确保你已将依赖数组指定为第二个参数!

🌐 Make sure you’ve specified the dependency array as a second argument!

如果你忘记了依赖数组,useMemo 每次都会重新运行计算:

🌐 If you forget the dependency array, useMemo will re-run the calculation every time:

function TodoList({ todos, tab }) {
// 🔴 Recalculates every time: no dependency array
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...

这是将依赖数组作为第二个参数传递的更正版本:

🌐 This is the corrected version passing the dependency array as a second argument:

function TodoList({ todos, tab }) {
// ✅ Does not recalculate unnecessarily
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...

如果这没有帮助,那么问题在于至少有一个依赖与上一次渲染不同。你可以通过手动将依赖记录到控制台来调试这个问题:

🌐 If this doesn’t help, then the problem is that at least one of your dependencies is different from the previous render. You can debug this problem by manually logging your dependencies to the console:

const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);

然后,你可以在控制台中对来自不同重新渲染的数组右键点击,并为它们都选择“存储为全局变量”。假设第一个保存为 temp1,第二个保存为 temp2,你就可以使用浏览器控制台检查两个数组中的每个依赖是否相同:

🌐 You can then right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. Assuming the first one got saved as temp1 and the second one got saved as temp2, you can then use the browser console to check whether each dependency in both arrays is the same:

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

当你发现哪个依赖破坏了记忆化时,要么找到方法去移除它,要么也对它进行记忆化。

🌐 When you find which dependency breaks memoization, either find a way to remove it, or memoize it as well.


我需要在循环中为每个列表项调用 useMemo,但这是不允许的

🌐 I need to call useMemo for each list item in a loop, but it’s not allowed

假设 Chart 组件被 memo 封装。当 ReportList 组件重新渲染时,你想要跳过列表中每个 Chart 的重新渲染。然而,你不能在循环中调用 useMemo

🌐 Suppose the Chart component is wrapped in memo. You want to skip re-rendering every Chart in the list when the ReportList component re-renders. However, you can’t call useMemo in a loop:

function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useMemo in a loop like this:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}

而是,为每个条目提取一个组件并为单个条目记忆化数据:

🌐 Instead, extract a component for each item and memoize data for individual items:

function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}

function Report({ item }) {
// ✅ Call useMemo at the top level:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}

或者,你可以移除 useMemo,而直接将 Report 封装在 memo. 中。如果 item 属性没有变化,Report 将跳过重新渲染,因此 Chart 也将跳过重新渲染:

🌐 Alternatively, you could remove useMemo and instead wrap Report itself in memo. If the item prop does not change, Report will skip re-rendering, so Chart will skip re-rendering too:

function ReportList({ items }) {
// ...
}

const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});