添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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 ,并返回最新结果。

  • useMemo 是一个 React Hook,所以你只能 在组件的顶层 或者自定义 Hook 中调用它。你不能在循环语句或条件语句中调用它。如有需要,将其提取为一个新组件并使用 state。
  • 在严格模式下,为了 帮你发现意外的错误 ,React 将会 调用你的计算函数两次 。这只是一个开发环境下的行为,并不会影响到生产环境。如果计算函数是一个纯函数(它本来就应该是),这将不会影响到代码逻辑。其中一次的调用结果将被忽略。
  • 除非有特定原因,React 不会丢弃缓存值 。例如,在开发过程中,React 会在你编辑组件文件时丢弃缓存。无论是在开发环境还是在生产环境,如果你的组件在初始挂载期间被终止,React 都会丢弃缓存。在未来,React 可能会添加更多利用丢弃缓存的特性——例如,如果 React 在未来增加了对虚拟化列表的内置支持,那么丢弃那些滚出虚拟化列表视口的缓存是有意义的。你可以仅仅依赖 useMemo 作为性能优化手段。否则,使用 state 变量 或者 ref 可能更加合适。
  • 注意

    这种缓存返回值的方式也叫做 记忆化(memoization) ,这也是该 Hook 叫做 useMemo 的原因。

    跳过代价昂贵的重新计算

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

    import { useMemo } from 'react';

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

    你需要给 useMemo 传递两样东西:

  • 一个没有任何参数的 calculation 函数 ,像这样 () => ,并且返回任何你想要的计算结果。
  • 一个由包含在你的组件中并在 calculation 中使用的所有值组成的 依赖列表
  • 在初次渲染时,你从 useMemo 得到的 将会是你的 calculation 函数执行的结果。

    在随后的每一次渲染中,React 将会比较前后两次渲染中的 所有依赖项 是否相同。如果通过 Object.is 比较所有依赖项都没有发生变化,那么 useMemo 将会返回之前已经计算过的那个值。否则,React 将会重新执行 calculation 函数并且返回一个新的值。

    换言之, useMemo 在多次重新渲染中缓存了 calculation 函数计算的结果直到依赖项的值发生变化。

    让我们通过一个示例来看看这在什么情况下是有用的

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

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

    如果计算速度很快,这将不会产生问题。但是,当正在过滤转换一个大型数组,或者进行一些昂贵的计算,而数据没有改变,那么可能希望跳过这些重复计算。如果 todos tab 都与上次渲染时相同,那么像之前那样将计算函数包装在 useMemo 中,便可以重用已经计算过的 visibleTodos

    这种缓存行为叫做 记忆化

    注意

    你应该仅仅把 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); // 如果 todos 和 tab 都没有变化,那么将会跳过渲染。
    }, [todos, tab]);
    console.timeEnd('filter array');

    useMemo 不会让首次渲染更快,它只会帮助你跳过不必要的更新工作。

    请记住,你的开发设备可能比用户的设备性能更强大,因此最好人为降低当前浏览器性能来测试。例如,Chrome 提供了 CPU Throttling 选项来降低浏览器性能。

    另外,请注意,在开发环境中测量性能无法为你提供最准确的结果(例如,当开启 严格模式 时,你会看到每个组件渲染两次而不是一次)。要获得最准确的时间,请构建用于生产的应用程序并在用户使用的设备上对其进行测试。

    深入探讨

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

    如果你的应用程序类似于此站点,并且大多数交互都很粗糙(例如替换页面或整个章节),则通常不需要使用记忆化。反之,如果你的应用程序更像是绘图编辑器,并且大多数交互都是颗粒状的(如移动形状),那么你可能会发现记忆化非常有用。

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

    • 你在 useMemo 中进行的计算明显很慢,而且它的依赖关系很少改变。
    • 将计算结果作为 props 传递给包裹在 memo 中的组件。当计算结果没有改变时,你会想跳过重新渲染。记忆化让组件仅在依赖项不同时才重新渲染。
    • 你传递的值稍后用作某些 Hook 的依赖项。例如,也许另一个 useMemo 计算值依赖它,或者 useEffect 依赖这个值。

    在其他情况下,将计算过程包装在 useMemo 中没有任何好处。不过这样做也没有重大危害,所以一些团队选择不考虑具体情况,尽可能多地使用 useMemo 。不过这种做法会降低代码可读性。此外,并不是所有 useMemo 的使用都是有效的:一个“永远是新的”的单一值就足以破坏整个组件的记忆化效果。

    在实践中,你可以通过遵循一些原则来避免 useMemo 的滥用

    1. 当一个组件在视觉上包裹其他组件时,让它 将 JSX 作为子组件传递 。这样,当包装器组件更新自己的 state 时,React 知道它的子组件不需要重新渲染。
    2. 首选本地 state,非必要不进行 状态提升 。例如,不要保持像表单、组件是否悬停在组件树顶部这样的瞬时状态。
    3. 保持你的 渲染逻辑纯粹 。如果重新渲染组件会导致一些问题或产生一些明显的视觉错误,那么它就是组件中的错误!修复错误而不是使用记忆化。
    4. 避免 不必要地更新 state 的 Effect 。React 应用程序中的大多数性能问题都是由 Effect 创造的更新链引起的,这些更新链导致组件反复重新渲染。
    5. 尽力 从 Effect 中移除不必要的依赖项 。例如, 相比于记忆化,在 Effect 内部或组件外部移动某些对象或函数通常更简单。

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

    使用 useMemo 和直接计算之间的区别

    1 示例 2 个挑战 :
    使用 useMemo 跳过重复计算

    在这个例子中, filterTodos 的执行被 人为减速了 ,这样就可以看到渲染期间调用的某些函数确实很慢时会发生什么。尝试切换选项卡并切换主题。

    切换选项卡会感觉很慢,因为它迫使减速的 filterTodos 重新执行。这是预料之中的效果,因为“选项卡”已更改,因此整个计算 需要 重新运行。如果你好奇为什么它会运行两次, 此处 对此进行了解释。

    然后试试切换主题。 useMemo 的帮助下,尽管已经被人为减速,但是它还是很快 !缓慢的 filterTodos 调用被跳过,因为 todos tab (你将其作为依赖项传递给 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>
            {visibleTodos.map(todo => (
              <li key={todo.id}>
                {todo.completed ?
                  <s>
    
    
    
    
        
    {todo.text}</s> :
                  todo.text
            ))}
        </div>