React手册 Hooks 之 useMemo
描述
React 官网对 useMemo 的描述原文
useMemo
is a React Hook that lets you cache the result of a calculation between re-renders.useMemo 是一个 React Hook 可以在你重新渲染之间缓存一个计算结果.
可以看出 useMemo 和 useCallback 非常类似, 都是起到缓存优化的作用, 区别在于 useMemo 用来缓存计算结果, 而 useCallback 用来缓存函数定义, 不过这只是 React 官方给出的解释, 如果你看过这两个方法的源码, 你会发现其实这两个方法也是可以混用的, useMemo 也可以缓存函数, 而 useCallback 也可以用来缓存变量或计算结果, 只不过这不符合 React 的预期(主要因为 React 认为一个匿名函数返回另一个匿名函数的定义看起来很笨重[确实]), 对后续的 React 更新并不友好, 所以 React 提出了一个专门针对函数的 useCallback.
场景
useMemo 是为了解决 React.memo 包装组件之后, 每次渲染父组件时, 给子组件传递的变量始终是全新的对象, 导致 React.memo 优化失效的问题, 它会在父组件外部缓存变量, 当父组件渲染时, 会返回缓存的变量, 让 React.memo 能拿到相同的变量引用, 从而让优化生效.
useMemo 和 useCallback 一样, 应该只用做性能优化, 如果你的代码没有它之后无法正常工作, 那就应该修复潜在问题, 然后用 useMemo 或 useCallback 提高性能.
接口定义:
参数
calculateValue: Function
计算需要缓存的值的函数, 应该是纯函数且不带有参数, 返回任意类型的值, React 会在初始渲染阶段调用这个函数, 并将结果缓存, 当 dependencies 发生变化时, 会再次调用并更新缓存中的结果, 然后从 useMemo 返回.
dependencies: Array<mixed>
calculateValue 函数中引用的所有触发更新的值的数组集合, 能够触发的值包括 props, state 以及直接在组件主体内声明的所有变量和函数. 这个数组的长度应该是固定的, 每次渲染时, React 会对数组内的值进行 Object.is 判断, 如果发生变化则重新计算新值, 如果没有变化, 则返回缓存中的值. 如果没有传递该值, 那么会导致 useMemo 始终返回新值, 如果传递空数组[], 那么 useMemo 会始终缓存该值.
返回
初始渲染时, useMemo 会返回 calculateValue 函数的返回值, 后续渲染中它会先判断依赖项是否发生变化, 如果变化就重新使用 calculateValue 计算新值并缓存, 如果没有变化, 那么返回缓存中的值.
用法1
跳过昂贵的重新计算, 要在重新渲染之间缓存计算结果, 可以像下面这样使用 useMemo
当 keyword 和 time 没有发生变化, 那么 useMemo 始终返回相同的结果, 这样可以跳过不必要的计算, 如果 filterBigList 的计算量很大的是时候, 效果会非常明显, 这样的缓存被称为记忆化(Memoization).
如果只在首次渲染时计算, 并且没有任何依赖项目, 代码看起来像这样: useMemo(() => { ... }, []); 也就是说计算之后希望能一直缓存结果, 那么可以使用 useState 或 useRef, 这种情况下可能 useMemo 并不是最优的选择, 原因是 React 官方给出的解释, 在未来的 React 规划中, 存在丢弃缓存的设计, 缓存的目的是为了跳过多余的渲染, 在一些特定的, 一定不会渲染的情况时, 缓存就是多余的, 如果你只是用 useMemo 来做优化, 这并没有什么影响, 但是如果担心丢失缓存对业务有影响, 那么可以使用 useState 和 useRef 来保证缓存.
用法2
跳过组件的重新渲染, 在默认情况下, 一个组件重新渲染时, React 会递归地重新渲染它的所有子组件, 这在大多数时候都是不必要的, 所以需要使用 React.memo 方法将组件包装, 包装后的组件, 在 props 没有变化的时候, 不会再进行重新渲染,
在例子中, 使用 React.memo 包装了子组件 Children 同时 Children 接收一个 data={childrenData} 的 props, 此时会发现 React.memo 并没有生效, 每次 MyApp 重新渲染时, 子组件 Children 依然会跟着渲染, 原因其实是 React.memo 的优化只有在 props 没有变化时才会生效, 而在例子中传递的 childrenData, 在每次重新渲染的时候, 都会获得一个全新的对象引用, 这样会导致 React.memo 对 props 进行 Obejct.is 对比的时候, 始终不会像等, 这样 React.memo 的优化就会失效.
所以需要使用下面这样使用 useMemo 来让 React.memo 优化生效
用法3
使用 useMemo 包装 JSX 节点, 因为 JSX 节点可以看作是一个类似 { type: "List", props: { ... } } 这样的对象, 那么如果是对象就可以被 useMemo 缓存(记忆).
这样的用法实际上和使用 const ListMemo = React.memo(List); 是一样的, 都是当 name 发生变化后重新渲染 List 组件, 区别在于 React.memo 可以指定一个判断函数(React.memo 的第二个参数), 而不仅仅是根据 props 进行浅比较, 而 useMemo 只能控制传递进去的 dependencies 相对而言没有 React.memo 灵活, 所以更推荐使用 React.memo 直接包装组件.
总结
useMemo 是一个可以将值记忆化(Memoization) 的一个 hook;
useMemo 需要谨慎使用, 管理好 dependencies 是优化的关键;
useMemo 不应该用来实现功能, 只用来做优化;
dependencies 不传会始终返回新值, 传空数组会在渲染一次之后一直缓存;
如果希望值在一次计算之后一直缓存, 没有依赖项目, 考虑使用 useState 和 useRef 代替;
useMemo 和 useCallback 在实现上并没有什么区别, 只是调用的方式不同;
虽然 useMemo 可以缓存 JSX 节点, 但是不推荐, 优先使用 React.memo 缓存组件.