useMemo 是一个 React Hook,它允许您缓存计算结果以避免在重新渲染时重复计算。

const cachedValue = useMemo(calculateValue, dependencies)

参考

useMemo(calculateValue, dependencies)

在组件的顶层调用 useMemo 来缓存计算结果,避免在重新渲染时重复计算。

import { useMemo } from 'react';

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

请参见下面的更多示例。

参数

  • calculateValue: 用于计算要缓存值的纯函数。它不应接受任何参数,并且应返回任何类型的数值。React 将在初始渲染期间调用您的函数。在下一次渲染时,如果 dependencies 自上次渲染以来没有更改,React 将再次返回相同的值。否则,它将调用 calculateValue,返回其结果,并将其存储以便稍后重用。

  • dependencies: calculateValue 代码中引用的所有反应式值的列表。反应式值包括 props、state 以及在组件主体中直接声明的所有变量和函数。如果您的代码检查器已针对 React 配置,它将验证每个反应式值是否都正确指定为依赖项。依赖项列表必须具有恒定的项数,并像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较将每个依赖项与其先前值进行比较。

返回值

在初始渲染中,useMemo 返回调用 calculateValue(无参数)的结果。

在下一次渲染过程中,它要么返回上次渲染时已存储的值(如果依赖项没有更改),要么再次调用 calculateValue,并返回 calculateValue 返回的结果。

注意事项

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

注意

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


用法

跳过昂贵的重新计算

要缓存重新渲染之间的计算,请将其包装在组件顶层的 useMemo 调用中。

import { useMemo } from 'react';

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

您需要将两件事传递给 useMemo

  1. 一个 计算函数,它不接受任何参数,例如 () =>,并返回您想要计算的内容。
  2. 一个 依赖项列表,其中包括组件中在计算内部使用的每个值。

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

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

换句话说,useMemo 会缓存重新渲染之间的计算结果,直到其依赖项发生更改。

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

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

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

通常情况下,这不算问题,因为大多数计算速度非常快。但是,如果您正在过滤或转换大型数组,或者正在进行一些代价高昂的计算,那么如果数据没有改变,您可能希望跳过再次进行计算。如果 todostab 与上次渲染时相同,则像前面一样将计算包装在 useMemo 中,可以让您重用之前已经计算过的 visibleTodos

这种类型的缓存称为 记忆化 (memoization)

注意

您应该只将 useMemo 用作性能优化。如果您的代码没有它就不能工作,请先找到根本问题并修复它。然后您可以添加 useMemo 来提高性能。

深入探讨

如何判断计算是否代价高昂?

一般来说,除非您正在创建或循环遍历数千个对象,否则这可能并不昂贵。如果您想获得更多信心,可以添加一个控制台日志来测量代码片段中花费的时间。

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

执行您正在测量的交互(例如,在输入框中键入)。然后,您将在控制台中看到类似于filter array: 0.15ms的日志。如果记录的总时间加起来是一个相当大的数目(例如,1ms或更多),则可能需要记忆该计算。作为一个实验,您可以将计算包装在useMemo中,以验证该交互的记录总时间是否减少。

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 节流选项。

另请注意,在开发环境中测量性能不会给出最准确的结果。(例如,当严格模式开启时,您会看到每个组件渲染两次而不是一次。)要获得最准确的时间,请构建您的应用程序以进行生产,并在类似于用户所拥有的设备上进行测试。

深入探讨

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

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

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

  • 您放在useMemo中的计算明显很慢,并且其依赖项很少更改。
  • 您将其作为道具传递给用memo包装的组件。如果值没有更改,您希望跳过重新渲染。记忆化允许您的组件仅在依赖项不同时才重新渲染。
  • 您传递的值随后用作某些 Hook 的依赖项。例如,另一个useMemo计算值可能依赖于它。或者,您可能依赖于来自useEffect的此值。

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

在实践中,您可以通过遵循一些原则来消除许多不必要的记忆化。

  1. 当组件视觉上包装其他组件时,让它接受 JSX 作为子元素。这样,当包装器组件更新自身状态时,React 就会知道其子元素不需要重新渲染。
  2. 优先使用局部状态,不要提升状态到超过必要的程度。例如,不要将表单之类的瞬态状态以及项目是否悬停在树的顶部或全局状态库中。
  3. 保持您的渲染逻辑纯净。如果重新渲染组件会导致问题或产生一些明显的视觉瑕疵,那就是您组件中的错误!修复错误而不是添加记忆化。
  4. 避免更新状态的不必要 Effects。React 应用程序中的大多数性能问题都是由源于 Effects 的更新链引起的,这些更新链导致您的组件一遍又一遍地渲染。
  5. 尝试从您的 Effects 中删除不必要的依赖项。例如,与其进行记忆化,不如将某些对象或函数移到 Effect 内部或组件外部通常更简单。

如果特定的交互仍然感觉很滞后,使用 React 开发者工具分析器查看哪些组件最受益于记忆化,并在需要时添加记忆化。这些原则使您的组件更容易调试和理解,因此无论如何都应该遵循它们。从长远来看,我们正在研究自动进行细粒度记忆化以一劳永逸地解决这个问题。

useMemo 和直接计算值之间的区别

示例 1 2:
使用useMemo跳过重新计算

在这个例子中,filterTodos 的实现被人为地放慢了速度,以便您可以看到当您在渲染过程中调用的某些 JavaScript 函数确实很慢时会发生什么。尝试切换选项卡并切换主题。

切换选项卡感觉很慢,因为它迫使减速的filterTodos重新执行。这是预期的,因为tab已更改,因此整个计算*需要*重新运行。(如果您好奇为什么它运行两次,这里有解释这里)。

切换主题。 感谢 useMemo,即使人为地减慢速度,它仍然很快! 缓慢的 filterTodos 调用被跳过了,因为 todostab(作为依赖项传递给 useMemo)自从上次渲染以来都没有改变。

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>
  );
}


跳过组件的重新渲染

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

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

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

默认情况下,当组件重新渲染时,React 会递归地重新渲染其所有子组件。 这就是为什么当 TodoList 使用不同的 theme 重新渲染时,List 组件也会重新渲染。对于不需要太多计算即可重新渲染的组件来说,这很好。但是,如果你已验证重新渲染很慢,你可以告诉 List 当其 props 与上次渲染时相同跳过重新渲染,方法是用 memo: 包装它。

import { memo } from 'react';

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

通过此更改,List 将跳过重新渲染,前提是其所有 props 与上次渲染时都相同。 这就是缓存计算变得重要的原因!想象一下,你没有使用 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}>
{/* ... so List's props will never be the same, and it will re-render every time */}
<List items={visibleTodos} />
</div>
);
}

在上面的示例中,filterTodos 函数始终创建一个不同的数组,类似于 {} 对象字面量始终创建一个新对象一样。通常情况下,这不会成为问题,但这意味着 List 的 props 将永远不会相同,并且你的 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 will receive the same props and can skip re-rendering */}
<List items={visibleTodos} />
</div>
);
}

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

深入探讨

记忆化单个 JSX 节点

无需用 memo 包装 List,你可以用 useMemo 包装 <List /> JSX 节点本身。

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 将不会重新渲染。

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

但是,如果 React 看到与上一次渲染时完全相同的 JSX,它就不会尝试重新渲染你的组件。这是因为 JSX 节点是 不可变的。 JSX 节点对象不可能随着时间的推移而改变,因此 React 知道跳过重新渲染是安全的。但是,为了使此方法有效,节点必须实际上是相同的对象,而不仅仅是在代码中看起来相同。这就是 useMemo 在此示例中所做的工作。

手动将 JSX 节点包装到 useMemo 中并不方便。例如,你无法有条件地执行此操作。这通常就是你用 memo 包装组件而不是包装 JSX 节点的原因。

跳过重新渲染和始终重新渲染之间的区别

示例 1 2:
使用useMemomemo跳过重新渲染

在这个例子中,List组件被人为地放慢了速度,以便您可以看到当您正在渲染的React组件真正变慢时会发生什么。尝试切换标签页并切换主题。

切换标签页感觉很慢,因为它强制使放慢速度的List重新渲染。这是预期的,因为tab已经改变,所以您需要在屏幕上反映用户的新选择。

接下来,尝试切换主题。由于使用了useMemo以及memo,即使人为地放慢了速度,它也很快!List跳过了重新渲染,因为自上次渲染以来visibleTodos数组没有改变。visibleTodos数组没有改变,因为todostab(您将其作为依赖项传递给useMemo)自上次渲染以来没有改变。

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>
  );
}


防止Effect过频繁触发

有时,您可能希望在Effect中使用一个值:

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

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

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

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

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

为了解决这个问题,您可以将需要从Effect中调用的对象包装在useMemo中。

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

const options = useMemo(() => {
return {
serverUrl: 'https://127.0.0.1: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对象在重新渲染之间是相同的。

但是,由于useMemo是性能优化,而不是语义保证,如果有特定原因这样做,React可能会丢弃缓存的值。这也会导致effect重新触发,因此最好消除对函数依赖项的需求,方法是将您的对象移到Effect内部。

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

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

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

现在您的代码更简单了,不需要useMemo了解有关删除Effect依赖项的更多信息。

记忆另一个Hook的依赖项

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

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对象的代码行也会在每次重新渲染时运行。由于searchOptionsuseMemo调用的依赖项,并且它每次都不同,React知道依赖项不同,并且每次都会重新计算searchItems

为了解决这个问题,您可以在将其作为依赖项传递之前,先记忆化searchOptions对象本身。

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计算函数内部。

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(它是一个字符串,不会“意外地”变得不同)。


记忆化函数

假设Form组件被包裹在memo中。你想将一个函数作为prop传递给它。

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

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

就像{} 创建一个不同的对象一样,函数声明,例如 function() {} 和表达式,例如 () => {} 在每次重新渲染时都会产生一个*不同的*函数。就其本身而言,创建一个新函数并不是问题,这并非需要避免的事情!但是,如果Form组件被记忆化了,那么你可能希望在没有prop发生变化时跳过重新渲染。一个*总是*不同的prop会破坏记忆化的目的。

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

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,以避免编写额外的嵌套函数。

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

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

上面这两个例子完全等效。useCallback 的唯一好处是它允许你避免在内部编写额外的嵌套函数。它不会做任何其他事情。阅读更多关于useCallback的内容。


疑难解答

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

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

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]);

// ...

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

这个仅限开发环境的行为可以帮助你保持组件的纯洁性。React使用其中一个调用的结果,并忽略另一个调用的结果。只要你的组件和计算函数是纯的,这就不应该影响你的逻辑。但是,如果它们意外地不纯,这将帮助你注意到并修复错误。

例如,这个不纯的计算函数会修改你作为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函数总是返回一个*不同的*数组,你可以修改*那个*数组。

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]);

阅读保持组件纯洁性以了解有关纯度的更多信息。

此外,请查看有关更新对象更新数组而不进行变异的指南。


我的useMemo调用应该返回一个对象,但是返回了undefined

这段代码不起作用。

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

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

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

但是,这仍然令人困惑,并且很容易被别人移除括号而破坏。

为了避免这个错误,请显式地编写一个return语句。

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

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

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

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

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

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

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

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

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

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

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 ...

当你找到哪个依赖项破坏了记忆化时,要么找到一种方法将其移除,要么也将其记忆化。


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

假设Chart组件被包裹在memo中。当ReportList组件重新渲染时,你想跳过列表中每个Chart的重新渲染。但是,你不能在循环中调用useMemo

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>
);
}

相反,为每个项目提取一个组件,并为各个项目记忆数据。

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也将跳过重新渲染。

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

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