useMemo 是一个 React 钩子,可让你在重新渲染之间缓存计算结果。

¥useMemo is a React Hook that lets you cache the result of a calculation between re-renders.

const cachedValue = useMemo(calculateValue, dependencies)

参考

¥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]
);
// ...
}

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • calculateValue:计算要缓存的值的函数。它应该是纯粹的,不带任何参数,并且应该返回任何类型的值。React 将在初始渲染期间调用你的函数。在下一次渲染中,如果 dependencies 自上次渲染以来没有改变,React 将再次返回相同的值。否则,它将调用 calculateValue,返回它的结果,并存储它以便以后可以重用。

    ¥calculateValue: The function calculating the value that you want to cache. It should be pure, should take no arguments, and should return a value of any type. React will call your function during the initial render. On next renders, React will return the same value again if the dependencies have not changed since the last render. Otherwise, it will call calculateValue, return its result, and store it so it can be reused later.

  • dependenciescalculateValue 代码中引用的所有反应值的列表。反应值包括属性、状态以及直接在组件主体内声明的所有变量和函数。如果你的 linter 是 为 React 配置,它将验证每个反应值是否正确指定为依赖。依赖列表必须具有恒定数量的条目,并且像 [dep1, dep2, dep3] 一样写成内联。React 将使用 Object.is 比较将每个依赖与其先前的值进行比较。

    ¥dependencies: The list of all reactive values referenced inside of the calculateValue code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison.

返回

¥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,所以你只能在你的组件的顶层或者你自己的钩子中调用它。你不能在循环或条件内调用它。如果需要,提取一个新组件并将状态移入其中。

    ¥useMemo is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.

  • 在严格模式下,React 会调用你的计算函数两次,以便 帮助你发现意外杂质 这是仅开发行为,不会影响生产。如果你的计算函数是纯的(应该是),这应该不会影响你的逻辑。其中一个调用的结果将被忽略。

    ¥In Strict Mode, React will call your calculation function twice in order to help you find accidental impurities. This is development-only behavior and does not affect production. If your calculation function is pure (as it should be), this should not affect your logic. The result from one of the calls will be ignored.

  • React 不会丢弃缓存的值,除非有特定原因这样做。例如,在开发中,当你编辑组件的文件时,React 会丢弃缓存。在开发和生产中,如果你的组件在初始挂载期间挂起,React 将丢弃缓存。将来,React 可能会添加更多利用丢弃缓存的功能 - 例如,如果 React 将来添加对虚拟化列表的内置支持,则丢弃滚动出虚拟化列表视口的条目的缓存是有意义的。如果你仅依赖 useMemo 作为性能优化,这应该没问题。否则,状态变量引用 可能更合适。

    ¥React will not throw away the cached value unless there is a specific reason to do that. For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache—for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should be fine if you rely on useMemo solely as a performance optimization. Otherwise, a state variable or a ref may be more appropriate.

注意

像这样缓存返回值也称为 记忆,,这就是此钩子称为 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. 一个 calculation function 不带任何参数,如 () =>,并返回你想要计算的内容。

    ¥A calculation function that takes no arguments, like () =>, and returns what you wanted to calculate.

  2. 依赖列表,包括组件中计算中使用的每个值。

    ¥A list of dependencies including every value within your component that’s used inside your calculation.

在初始渲染中,你将从 useMemo 获得的 value 将是调用你的 calculation 的结果。

¥On the initial render, the value you’ll get from useMemo will be the result of calling your calculation.

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

¥On every subsequent render, React will compare the dependencies with the dependencies you passed during the last render. If none of the dependencies have changed (compared with Object.is), useMemo will return the value you already calculated before. Otherwise, React will re-run your calculation and return the new value.

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

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

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

¥Let’s walk through an example to see when this is useful.

默认情况下,React 会在每次重新渲染时重新运行组件的整个主体。例如,如果此 TodoList 更新其状态或从其父级接收新属性,则 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 来提高性能。

¥You should only rely on useMemo as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first. Then you may add useMemo to improve performance.

深入研究

如何判断计算是否昂贵?

¥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 不会使第一次渲染更快。它只会帮助你跳过不必要的更新工作。

¥useMemo won’t make the first render faster. It only helps you skip unnecessary work on updates.

请记住,你的机器可能比用户的机器快,因此最好通过人为减速来测试性能。例如,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 中进行的计算明显很慢,而且它的依赖很少改变。

    ¥The calculation you’re putting in useMemo is noticeably slow, and its dependencies rarely change.

  • 你将它作为属性传递给封装在 memo 中的组件。如果该值未更改,你希望跳过重新渲染。记忆化让你的组件仅在依赖不同时才重新渲染。

    ¥You pass it as a prop to a component wrapped in memo. You want to skip re-rendering if the value hasn’t changed. Memoization lets your component re-render only when dependencies aren’t the same.

  • 你传递的值稍后用作某些钩子的依赖。例如,也许另一个 useMemo 计算值取决于它。或者,也许你依赖于 useEffect. 的这个值

    ¥The value you’re passing is later used as a dependency of some Hook. For example, maybe another useMemo calculation value depends on it. Or maybe you are depending on this value from 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.

在实践中,你可以通过遵循一些原则来避免大量记忆化:

¥In practice, you can make a lot of memoization unnecessary by following a few principles:

  1. 当一个组件在视觉上封装其他组件时,让它 接受 JSX 作为子级。 这样,当封装器组件更新自己的状态时,React 知道它的子级不需要重新渲染。

    ¥When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.

  2. 首选本地状态,除非必要,否则不要 提升状态。例如,不要保留像表单这样的瞬时状态,也不要保留某个条目是否悬停在树的顶部或全局状态库中。

    ¥Prefer local state and don’t lift state up any further than necessary. For example, don’t keep transient state like forms and whether an item is hovered at the top of your tree or in a global state library.

  3. 保留你的 渲染逻辑纯粹。 如果重新渲染组件导致问题或产生一些明显的视觉伪像,那是你的组件中的错误!修复错误而不是添加记忆化。

    ¥Keep your rendering logic pure. If re-rendering a component causes a problem or produces some noticeable visual artifact, it’s a bug in your component! Fix the bug instead of adding memoization.

  4. 避免 更新状态的不必要的副作用。 React 应用中的大多数性能问题都是由副作用引起的更新链引起的,这些更新链导致组件反复渲染。

    ¥Avoid unnecessary Effects that update state. Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.

  5. 尝试以 从你的副作用中删除不必要的依赖。 例如,在副作用内部或组件外部移动某些对象或函数通常更简单,而不是记忆化。

    ¥Try to remove unnecessary dependencies from your Effects. For example, instead of memoization, it’s often simpler to move some object or a function inside an Effect or outside the component.

如果特定的交互仍然感觉滞后,使用 React 开发者工具分析器 看看哪些组件可以从记忆化中获益最多,并在需要的地方添加记忆化。这些原则使你的组件更易于调试和理解,因此在任何情况下都遵循它们是很好的。从长远来看,我们正在研究 自动进行粒度记忆化 以一劳永逸地解决这个问题。

¥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 / 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 作为属性传递给子 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 会递归地重新渲染它的所有子级。这就是为什么当 TodoList 使用不同的 theme 重新渲染时,List 组件也会重新渲染。这对于不需要太多计算来重新渲染的组件来说很好。但是如果你已经确认重新渲染很慢,你可以告诉 List 在它的属性与上次渲染相同时通过将它封装在 memo 中来跳过重新渲染

¥By default, when a component re-renders, React re-renders all of its children recursively. This is why, when TodoList re-renders with a different theme, the List component also re-renders. This is fine for components that don’t require much calculation to re-render. But if you’ve verified that a re-render is slow, you can tell List to skip re-rendering when its props are the same as on last render by wrapping it in memo:

import { memo } from 'react';

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

通过此更改,如果 List 的所有属性都与上次渲染相同,则将跳过重新渲染。这就是缓存计算变得重要的地方!假设你计算了没有 useMemovisibleTodos

¥With this change, List will skip re-rendering if all of its props are the same as on the last render. This is where caching the calculation becomes important! Imagine that you calculated visibleTodos without useMemo:

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}>
{/* ... so List's props will never be the same, and it will re-render every time */}
<List items={visibleTodos} />
</div>
);
}

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

¥In the above example, the filterTodos function always creates a different array, similar to how the {} object literal always creates a new object. Normally, this wouldn’t be a problem, but it means that List props will never be the same, and your memo optimization won’t work. This is where useMemo comes in handy:

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 will receive the same props and can skip re-rendering */}
<List items={visibleTodos} />
</div>
);
}

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

¥By wrapping the visibleTodos calculation in useMemo, you ensure that it has the same value between the re-renders (until dependencies change). You don’t have to wrap a calculation in useMemo unless you do it for some specific reason. In this example, the reason is that you pass it to a component wrapped in memo, and this lets it skip re-rendering. There are a few other reasons to add useMemo which are described further on this page.

深入研究

记忆化单个 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 / 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

有时,你可能希望在 副作用 中使用一个值

¥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();
// ...

这就产生了一个问题。每个反应值都必须声明为副作用的依赖。 但是,如果你将 options 声明为依赖,则会导致你的副作用不断重新连接到聊天室:

¥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 中:

¥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 options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Only changes when createOptions changes
// ...

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

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

但是,由于 useMemo 是性能优化,而不是语义保证,因此如果 这样做是有特定原因的,React 可能会丢弃缓存的值。这也会导致效果重新触发,因此最好通过将对象移动到副作用内部来消除对函数依赖的需要:

¥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详细了解如何删除副作用依赖。

¥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(这是一个字符串,“accidentally” 不能变得不同)。

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


记忆化一个函数

¥Memoizing a function

假设 Form 组件封装在 memo 中,你想将一个函数作为属性传递给它:

¥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 组件被记忆化,你可能想在没有属性发生变化时跳过重新渲染它。总是不同的属性会破坏记忆化点。

¥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 调用你的函数两次,所以你会注意到 todo 被添加了两次。你的计算不应更改任何现有对象,但可以更改你在计算期间创建的任何新对象。例如,如果 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>
);
});

React 中文网 - 粤ICP备13048890号