最新react官方文档翻译日记--useMemo


title: useMemo

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 将会返回相同的值。 否则,React 将会调用 calculateValue 函数返回它的结果并且将其缓存以便下一次重新使用。

  • dependencies:有关 calculateValue 内部代码的响应值的一个列表。响应值包含 prop、state,和所有在你组件内部直接声明的函数和变量。如果你代码检查工具是 configured for React,那么它将校验每一个正确被指定为依赖的值。依赖列表必须有确切数字的项并且可以以 [dep1, dep2, dep3] 的形式编写。React 将会使用 Object.is 比较算法将每一个依赖与其先前值相对比。

返回值

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

在接下来的渲染中,useMemo 返回在上一次渲染中已经缓存的值(如果 dependencies 还没有发生改变的话),或者再次调用 calculateValue,并且返回 calculateValue 已经返回的结果。

警告 {/caveats/}
  • useMemo 是一个Hook,所以你能在你 组件的顶层 或者你自定义的Hooks中调用。你不能在循环或者条件语句中调用它。如果你需要这样做,新建一个组件,并且将状态移入其中。
  • 在严格模式中, React 将会调用你的计算函数两次,目的是帮助你找到偶然的杂质。这仅是开发环境中的行为并且不会影响到生产环境。如果你的计算函数是纯的(理应如此),那么它不应该影响到你的逻辑。其中一个调用结果将会被忽略。
  • React 将不会丢弃任何缓存值除非有特定的理由这样做. 比如,在开发环境中,当你编辑你组件的文件时,React 会丢弃缓存.在生产和开发环境中,如果你的组件在初次挂载中暂停,React将会丢弃缓存。将来, React 增加更多利用丢弃缓存的特性----例如, 如果 React 在将来内置了对虚拟列表的支持,那么丢弃那些超出虚拟化表视图的项是有意义的.如果你仅依赖 useMemo 作为性能优化的途径,这将会对你有帮助. 不然, 使用state variable或者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 组件更新了它的 state 或者从它的父组件处接收到了新的 props, filterTodos 函数将会再次运行:

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

通常情况下,这都是没有问题的,因为大多数计算都是十分迅速的。然而,如果你在过滤或者转换一个庞大的数组,或者在做一些昂贵的计算,你可能想要再次跳过计算如果数据没有改变。 如果 todostab 从上次渲染以来都没有发生改变,那么将计算函数像先前一样包裹在 useMemo 中可以重用你此前已经计算了的 visibleTodos 值。
这种缓存类型叫做 memoization.

你应该只依赖 useMemo 作为性能优化。如果你的代码在没有它的情况不能运行,请找到根本原因并且首先修复它。 然后你可以添加 useMemo 以改善性能。

如何去判断一个计算是昂贵的? {/how-to-tell-if-a-calculation-is-expensive/}

通常, 除非你正在创建或者循环访问数以千计的对象,否则计算并不昂贵的。如果你需要获取更多信息,你可以添加一个 console log 语句去测量在一段代码中花费的时间:

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); // 如果 todos 和 tab 没有改变,这里会跳过
}, [todos, tab]);
console.timeEnd('filter array');

useMemo 不会使初次渲染变快。它只会帮你在更新时跳过没有必要的计算。

请记住,你的设备速度可能快于你用户设备的速度。所以使用人为减速去测试性能是一个好方法。比如, Chrome 提供了一个 CPU Throttling 选项去做这些。

另请注意,在生产环境中测试性能将不会得到最精确的结果。(举例来说,当严格模式开启时, 你将会看到每一个组件渲染了两次而不是一次。)要获取更为准确的时间,将你的应用构建用于生产环境并且在和你用户类似的设备上进行测试。

你应该在所有地方都添加 useMemo 吗 {/should-you-add-usememo-everywhere/}

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

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

  • 你放入 useMemo 的计算函数明显很慢,并且它的依赖很少改变。
  • 你将计算函数传给一个包裹在memo中的组件。 如果计算函数的值没有改变,你想要跳过重新渲染。Memoization 让你的组件只有在依赖改变的时候才会重新渲染。
  • 你传递的值稍后将用作某个 Hook 的依赖项。譬如,另一个 useMemo 的计算函数值可能依赖于它。 或者你可能在 useEffect. 中依赖这个值。

其他情况下将计算函数包裹在 useMemo 中没有任何益处。那样做也没有很大的害处。所以有些团队选择不考虑个案,尽可能多地记忆,这个方法的缺陷是代码可读性降低。 而且,并不是所有的记忆都是有效的:总是“新的”单个值足够破坏整个组件的记忆。

实际上,你可以通过遵循以下几个原则来使大量记忆变得不必要:

  1. 当一个组件包含包含其他组件时,让其接受 JSX 作为子元素。通过这种方式,当包含者组件更新它自身状态时,React知道它的子组件不需要再次渲染。
  2. 推荐本地状态,并且不要提升状态超过必要的程度。比如,不要保留像表格等瞬时状态,以及项是否在组件树顶部还是在全局状态库中。
  3. 保持你渲染逻辑纯净。 如果重新渲染组件产生问题或者生成了一些明显的视觉伪影。这是你组件自身的 bug!找到这个bug,而不是添加记忆。
  4. 避免 不必要的更新状态的副作用。大多数React应用的性能问题都是由副作用更新链引起的,那让你的组件一遍又一遍渲染。
  5. 尝试移除你副作用中不必要的依赖。例如,移除副作用内部或者组件外部的对象或者函数通常更简单,而不是而不是记忆。

如果特地的交互仍然感觉很子厚。 使用 React 开发者工具 去查看哪个组件在记忆中受益是最大的。 并且在需要时添加记忆。这些原则让你的程序更加易于调试和便于理解,所以最好在任何情况下遵循它们。从长远看, 我们正在研究自动记忆以一劳永逸解决这个问题。

使用 useMemo 跳过计算

在这个例子中,filterTodos 的实现被人为减慢了速度,如此你可以看到你正在调用的渲染中很慢函数发生了什么。尝试切换选项卡并切换主题。

切换选项卡的很慢, 因为它迫使减慢的 filterTodos 去再次执行。那正是预想的那样,因为 tab 已经发生改变,所以整个计算需要再次执行。(如果你很好奇为什么它运行了两次,这里解释了原因)

更改主题。 得益于 useMemo,尽管他被就减慢了速度但是还是运行得很快! 慢的 filterTodos 调用被跳过, 因为 todostab(你传递给 useMemo 的依赖) 自从上次渲染以来都没有发生改变。

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

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>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // 500毫秒内什么也不做来模拟特别慢的情况
  }

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}
总是重新计算值

本例中, filterTodos 的实现也被认为减慢了速度 如此你可以看到你正在调用的渲染很慢的函数发生了什么。尝试切换选项卡并切换主题

不像前例, 现在切换主题也很慢! 那是因为 这次没有调用 useMemo, 所以被人为减慢速度的 filterTodos 在每一次渲染中都被调用。即使只有 theme 发生改变,他也会被调用。

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
      // 500毫秒内什么也不做来模拟特别慢的情况
  }

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}

然而,这是相同的代码,只是被人减慢速度的部分被移除 ,缺少 useMemo 感觉是否很明显

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('Filtering ' + todos.length + ' todos for "' + tab + '" tab.');

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}

十分迅速,没有记忆的代码运行地很好。 如果你的交互运行的足够快,你可能需要记忆。

你可以在 utils.js 中尝试增加 todo 项,并且观察行为是如何改变的。这个特别的计算在开始时可能不是特别昂贵, 但是如果 todos 项的数量增长到十分庞大时,大量开销将用于重新渲染热不是过滤。请继续阅读下文去了解怎样使用 useMemo 优化重新渲染。


跳过组件的重新渲染 {/skipping-re-rendering-of-components/}

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

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

你可能注意到切换 theme prop 让应用停滞了一会, 但是如果你移除 <List /> 组件,运行得很快。这提示你优化 List 组件是有意义的。

默认情况下, 当一个组件重新渲染时,React将会递归渲染它的子组件。这就是为什么当带有不同 theme 值的 TodoList重新渲染时,List 重新渲染。这对于不需要大量计算去重新渲染的组件来说影响很小。但如果你发现某次重新渲染很慢,你可以将 List 组件包裹在 memo: 中。当它的 props 和上一个渲染相同时,告知 List 组件跳过重新渲染。

import { memo } from 'react';

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

有了这个改变,如果 List 所有的props自上次渲染以来都相同的话,它将会跳过重新渲染。 这时候缓存计算结果就变得很重要了!试想一下你没有使用 useMemo 去计算 visibleTodos

export default function TodoList({ todos, tab, theme }) {
  //每一次主题改变时,都会有一个新的数组
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      {/* ... 所以 List 的 props 永远不可能相同,那么它每一次都会重新渲染 */}
      <List items={visibleTodos} />
    </div>
  );
}

在整个例子中, filterTodos 函数总会生成一个新的数组, 就像 {} 对象字面量总是创造一个新对象一样。 通常情况下,这不成问题,但是者意味着 List 的props 将永远不是相同的, 并且你的 memo 优化不会起作用。 这就是 useMemo 派上用场的地方:

export default function TodoList({ todos, tab, theme }) {
  // 告知react在多次重新渲染中缓存你的计算结果
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // 只要这些依赖没有发生改变
  );
  return (
    <div className={theme}>
      {/* List 将会收到相同的props并且可以跳过重新渲染 */}
      <List items={visibleTodos} />
    </div>
  );
}

通过将 visibleTodos 计算函数包裹在 useMemo 中,你确保 visibleTodos 在多次重新渲染中有相同的值 (直到依赖发生改变)。 你不必 将一个计算函数包裹在 useMemo 中,除非你有特定的理由那样做。在本例中,理由是在本例中,理由是你将他传递到了包裹在 memo 中的组件,这允许它跳过重新渲染.还有其他原因你可能需要用到 useMemo,本页将对此进行进一步描述。

记忆虚拟的 JSX 节点

除了将 List 包裹在 memo 中,你可以将 <List /> JSX 节点自身包裹在 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 将不会重新渲染。

<List items={visibleTodos} /> 的 JSX 节点是一个 { type: List, props: { items: visibleTodos } } 一样的对象。创建这个对象消耗十分低,但是 React 不知道它的内容是否和上次一样。 这是因为在默认情况下, React 将重新渲染 List 组件。

但是, 如果 React 看到的 JSX 节点与上一次渲染时完全相同,他将不会去渲染你的组件。 这是因为 JSX 节点是不变的。 一个 JSX 节点不能随着时间而改变, 所以 React 跳过重新渲染是安全的。然而,为了实现这一点,节点必须实际上是相同的对象,而不仅仅是在代码中看起来相同。这就是 useMemo 在本例中的作用。
手动将 JSX 节点包裹在 useMemo 中并不便捷。例如,你不能有条件地这样做。 这就是为什么你一般使用 memo 包裹组件而不是包裹 JSX 节点。

使用 useMemomemo来跳过重新渲染{/skipping-re-rendering-with-usememo-and-memo/}

在本例中,List 被人为减慢速度 如此你可以看到你正在调用的渲染很慢的函数发生了什么。 尝试切换选项卡并且切换主题。

切换选项卡感到十分缓慢, 因为它强制被减速了的 List 组件去重新渲染。 这是意料之中的事,因为 tabs 已经更改,因此你需要在屏幕上反映用户的新选择。

接下来,尝试更改主题。有了 useMemomemo的共同作用,尽管他被人为减速,但是还是运行的很快! List 组件跳过了重新渲染 因为 visibleItems 数组自从上次渲染以来都没有发生改变。 visibleItems 数组没有变化因为 todostab (你传递给 useMemo的依赖)自从上次渲染以来都没有发生改变

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
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>
  );
}
import { memo } from 'react';

const List = memo(function List({ items }) {
  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // 500毫秒什么也不做来模拟运行的极其慢的情况
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
});

export default List;
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}
总是渲染不同组件

在本例中, List 的实现也被也被人为减速了 ,如此当某些你正在渲染的 React 组件在特别慢的时候你可以看到发生了什么。尝试切换选项卡和主题。

不像先前示例,现在切换主题也很慢! 那是意味 这个版本里面没有 useMemo 的调用, 所以 visibleTodos 总是一个不同的数组, 并且被减速的 List 组件 不能跳过重新渲染。

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import List from './List.js';
import { filterTodos } from './utils.js'

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

const List = memo(function List({ items }) {
  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // 500毫秒什么也不做来模拟运行的极其慢的情况
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
});

export default List;
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}

然而,这是删除了人为减速的 相同代码,缺少 useMemo 是否感觉明显?

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import List from './List.js';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}
import { memo } from 'react';

function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
}

export default memo(List);
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

.dark {
  background-color: black;
  color: white;
}

.light {
  background-color: white;
  color: black;
}

十分迅速, 没有记忆的代码运行地很好。如果你的记忆已经足够快了,你并不需要记忆。

请记住你需要将 React 运行在生产模式下, 关闭 React 开发者工具,使用与你应用用户相似的设备去了解到底是什么减速了你的应用速度


记忆另一个 Hook 的依赖

假设你有一个计算赖了在组件直接内部创建的对象:

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

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // 🚩 注意:依赖了一个直接在组件内部创建的对象
  // ...

依赖一个像这样的对象破坏了记忆点。当组件重新渲染时, 组件内部所有的代码都会重新运行。 创建 searchOptions 对象的代码段也会在每一次重新渲染时运行。 因为 searchOptions 是你的 useMemo 调用的一个依赖, 并且它每次都会不同, React 知道了依赖的不同,并且每一次都重新计算 searchItems

为了修复这个,你应该记忆 searchOptions 对象本身,在将它传递作为依赖之前:

function Dropdown({ allItems, text }) {
  const searchOptions = useMemo(() => {
    return { matchMode: 'whole-word', text };
  }, [text]); // ✅ 只有在 text 改变的时候才会发生改变

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // ✅ 只有在 allItems 或者 searchOptions 改变的时候才会发生改变
  // ...

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

function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]); // ✅ 只有在 text 或者 searchOptions 改变时才会发生改变
  // ...

现在你的计算函数直接依赖 text (一个不能“意外”发生改变的字符串).


记忆一个函数 {/memoizing-a-function/}

假设 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/' + product.id + '/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/' + product.id + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

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

上面两个例子都是完全等价的。 useCallback 唯一的益处就是它避免让你在内部写一个额外的嵌套函数。它不会有其他的行为。 阅读关于 useCallback 更多信息


疑难解答

我的计算函数每一次渲染都运行两次

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

function TodoList({ todos, tab }) {
  // 这个组件函数在每一次渲染的时候将会运行两次

  const visibleTodos = useMemo(() => {
    // 如果任何依赖改变,这个计算函数将会运行两次
    return filterTodos(todos, tab);
  }, [todos, tab]);

  // ...

这是意料之中的,并且不应该破坏你的代码。

这个 仅限开发模式 行为 帮助你保持组件的纯正。 React 使用仅其中一次调用结果,并且忽略掉另一个调用结果。 只要你的组件和计算函数是纯正的,那么这个不应该影响到你的逻辑。 但是,如果它们意外发生不纯洁情况,这将帮助你注意到,并且修复他。

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

  const visibleTodos = useMemo(() => {
    // 🚩 错误: 改变一个 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);
    // ✅ 正确: 改变你在计算中创建的对象
    filtered.push({ id: 'last', text: 'Go for a walk!' });
    return filtered;
  }, [todos, tab]);

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

另外, 查看有关更新对象更新数组(无突变)


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

这个代码将不会运行:

  // 🔴 你不应该是用 () => { 从一个箭头函数中返回一个对象 
  const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
  }, [text]);

在JavaScript中, () => { 是函数体的开始, 所以这 { 支撑不再是你对象的一部分。这就是为什么它不返回一个对象,并且导致错误。你可以通过添加像 ({}) 的括号去修复:

  // 这会有效, 但是很容易再次崩溃
  const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
  }), [text]);

然而, 这仍然是令人疑惑的并且通过移除括号很容易崩溃

To avoid this mistake, write a return statement explicitly:

  // ✅ 这有效并且是明确的
  const searchOptions = useMemo(() => {
    return {
      matchMode: 'whole-word',
      text: text
    };
  }, [text]);

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

确认你已经指定了依赖数组作为第二个参数!

如果你遗忘了依赖数组, useMemo 将会每一次都调用计算函数:

function TodoList({ todos, tab }) {
  // 🔴 每一次都重新计算: 没有依赖数组
  const visibleTodos = useMemo(() => filterTodos(todos, tab));
  // ...

传递依赖数组作为第二个参数是正确的:

function TodoList({ todos, tab }) {
  // ✅ 必要时才会渲染
  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]); // 数组之间的第一个依赖项是否相同?
Object.is(temp1[1], temp2[1]); // 数组之间的第一个依赖项是否相同?
Object.is(temp1[2], temp2[2]); // 数组中的每一个依赖是否相同?

当你发现每一个依赖破坏了记忆,寻找一个方式去移除它, 或者将其也记忆起来


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

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

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 你不能像这样在循环中调用 useMemo:
        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 }) {
  // ✅ 在顶部调用 useMemo
  const data = useMemo(() => calculateReport(item), [item]);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
}

或者, 你可以移除 useMemo 并且在 memo 中包裹 Report 组件自身。如果 item prop 没有发生改变, Report 组件将会跳过重新渲染, 如此 Chart 也会跳过重新渲染:

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

const Report = memo(function Report({ item }) {
  const data = calculateReport(item);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值