你可能已经注意到 React Hook 中有一个名为 useMemo 的奇怪的钩子。这个奇怪的钩子意味着什么,它的作用是什么?重要的是,它是怎样为你提供帮助的?
首先,稍微回顾一下 JavaScript 的相等性。
引用比较
你可能还记得 Javascript 如何比较对象。当我们进行相等性比较时,会有一些棘手的结果:
{} === {} // false
const z = {}
z === z // true
React 用 Object.is 来比较组件,但是得到的结果与使用 === 相似。所以当 React 检查组件中的改变时,它可能会发现一些我们不会真正考虑的东西。
() => {} === () => {} // false
[] === [] // false
这种比较检查将会导致某些预期之外的 React 重新渲染。如果重新渲染是一些代价高昂的操作,则可能会降低性能。如果一部分需要进行重新渲染,则它将重新渲染整个组件树。因此 React 发布了 memo 来解决这个问题。
Memoization
有一个非常花哨的术语 memoization 。memoization 是一种“优化技术”,它传递了一个复杂的函数来进行记忆。在 memoization 中,当随后传递的参数相同时,它会记住结果。例如有一个计算 1 + 1 的函数,它将返回结果 2。但是如果它使用 memoization,则下次再通过该函数运行 1 + 1 时,它不会再次进行运算,而只会记住答案是 2,从而无需执行加法函数。
在 React 中,memoization 可以优化我们的组件,避免在不需要时进行复杂的重新渲染。例如可以用 React.memo 对程序进行优化,它就像一个纯组件一样,可以包装你的组件。但是 useMemo 的用法有些不同。
在官方的React文档中,useMemo 是这样子的:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 接受一个函数和一个依赖关系列表(数组 [a,b])。它们的行为类似于函数中的参数。依赖关系列表是 useMemo 要去监视的元素:如果没有改变,那么函数的结果将会保持不变,否则它将重新运行这个函数。假如它们没有改变的话,那么重新渲染整个组件也没关系,该函数不会被重新执行,而是直接返回存储的结果。如果包装的函数很大且很运行代价高昂,那么这绝对是一个非常好的方案。这就是 useMemo 的主要用途。
useMemo 示例
const List = useMemo(
() =>
listOfItems.map(item => ({
…item,
itemProp1: expensiveFunction(props.first),
itemProp2: anotherPriceyFunction(props.second)
})),
[listOfItems]
)
在上面的例子中,useMemo 函数将在第一个渲染器上运行。它会阻塞线程,直到函数执行完毕,因为 useMemo 在渲染器中运行。它看起来不如 useEffect 干净,因为 useEffect 可以渲染加载微调器,直到运行代价高昂的函数完成并且效果消失为止。但是如果 listOfItems 从未被改变,那么函数将永远不会再次触发,仍然会获取返回值。这样会使这些函数的执行速度显得很快。这是你在执行高耗时的同步函数时的理想选择。
防止重新渲染
如果你熟悉 React 的类组件生命周期 Hook shouldComponentUpdate,useMemo 在防止不必要的重新渲染方面也有类似用法。假设有以下代码:
function BabyGator({fish, insects}) {
const dinner = {fish, insects};
useEffect(() => {
eatFunction(dinner);
}, [fish, insects])
}
function MamaGator() {
return
}
它工作得很好。useEffect hook 监视传入的 fish 和 insects。但是这仅适用于 primitive 值。这是关键。
还记得前面提到的“引用比较”吗: [] === [] // false。这正是 useMemo 和 useCallback 之类的记忆 hook 所做的事。如果的 insects 是一个数组,我们可以把它放在 useMemo hook 中,在渲染之后,它将相等地引用它。如果一个函数或另一个非原始值位于 useEffect 依赖项中,由于closure 的原因,它将会重新创建一个新数组,并且发现它不相等。
很显然,如果我们只是想存储数组就不需要 useMemo。但是如果有一个代价高昂的函数来计算这个数组,useMemo是很有用的。
什么时候不能用 useMemo
useCallback 类似于 useMemo,但是它返回一个被记忆的函数,而 useMemo 有一个返回 value 的函数。如果依赖项数组为空,则不可能进行记忆,它将在每个渲染器上去计算新的值。在这时你最好实现 useRef 钩子。如果依赖项发生更改,则 useMemo 比 useRef 优秀的一点是能够重新进行存储。
在实现 useMemo 时,你需要问问自己:“这真的是一个代价高昂的函数吗?” 代价高昂意味着它正在消耗大量资源(如内存)。如果在渲染时在函数中定义大量变量,则用 useMemo 进行记忆是非常有意义的。
如果你不希望 useMemo 去触发有副作用的操作或是异步调用。使用 useEffect 中会更有意义。
当你想要使用 useMemo 时,请先编写代码,然后再检查是否可以对其进行优化。不要一开始就去使用 useMemo 开头。这样可能会在小型应用中导致性能变差。