useMemo 是 React Hooks 中的一个非常有用的钩子,它可以帮助你在组件渲染中优化计算的成本。下面是对 useMemo 的解释和用法:
解释
useMemo 用于“记忆化”计算结果。这意味着如果依赖数组中的值没有发生变化,该钩子可以避免重新计算并返回上一次的值。这对于性能成本较高的计算尤其有用,因为它可以确保这些计算只在必要的时候执行。
用法
useMemo 的基本语法是:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 第一个参数是一个函数,这个函数返回你想要“记忆化”的值。
- 第二个参数是一个依赖数组,只有当数组中的值发生变化时,上述函数才会重新执行。如果没有提供这个数组,那么函数会在每次组件渲染时都重新执行。
示例
假设我们有一个性能成本较高的函数 computeExpensiveValue,该函数根据两个参数 a 和 b 计算一个值。我们可以使用 useMemo 来确保只在 a 或 b 发生变化时才重新计算:
import React, { useMemo } from 'react';
function ExpensiveComponent({ a, b }) {
const value = useMemo(() => {
console.log("Recomputing value...");
return computeExpensiveValue(a, b);
}, [a, b]);
return <div>{value}</div>;
}
function computeExpensiveValue(a, b) {
return a + b;
}
在上述例子中,每次 a 或 b 发生变化时,控制台都会输出 "Recomputing value..."。但如果这两个属性在渲染之间没有发生变化,那么 useMemo 会直接返回上次计算的值,从而避免了不必要的重新计算。
以上案例将useMemo基本功能讲解完毕,下面我们进行useMemo源码分析
源码在packages/react-reconciler/src/ReactFiberHooks.js 中可以找到
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null
): T {
// 获取当前的hook状态。
const hook = updateWorkInProgressHook();
// 根据提供的`deps`参数来处理,并获取新的依赖数组。
const nextDeps = deps === undefined ? null : deps;
// 从当前的hook对象中提取之前保存的状态值和依赖项。
const prevState = hook.memoizedState;
// 如果提供了新的依赖数组,检查它是否与旧的依赖数组不同。
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 函数 areHookInputsEqual 的主要目的是判断两个依赖数组(nextDeps 和 prevDeps)是否相等。
// 下面会单独讲解这个函数
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果新旧依赖数组相同,返回保存的值。
return prevState[0];
}
}
// 双重调用是开发模式下的一个特性,用于捕获某些副作用。
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
// 如果依赖发生了变化或没有提供依赖数组,调用`nextCreate`以获取新值。
const nextValue = nextCreate();
// 更新hook的状态,保存新的值和新的依赖数组。
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
总结:
updateMemo函数的工作方式是:首先,它会获取当前hook的状态。然后,根据是否提供了deps参数,它会处理并确定新的依赖数组。接下来,从当前的hook对象中,函数会提取之前保存的状态值和依赖项。如果提供了新的依赖数组,函数会检查新的依赖数组是否与旧的依赖数组不同。如果新旧依赖数组相同,函数会返回保存的值。另外,在开发模式下,还有一个名为shouldDoubleInvokeUserFnsInHooksDEV的特性,它可能会使函数执行双重调用,这在捕获某些副作用时是有用的。如果依赖发生了变化或根本没有提供依赖数组,函数会调用nextCreate以获取新值。最后,函数会更新hook的状态,保存新的值和新的依赖数组,并返回新计算的值或已经保存的值。
分析areHookInputsEqual函数
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null
): boolean {
/** 检查热重载条件 */
if (__DEV__) {
// 如果ignorePreviousDependencies为true的话 说明此组件正在进行热重载 并且该函数立即返回false
if (ignorePreviousDependencies) {
return false;
}
}
// 如果prevDeps是null 则输出一个错误消息并返回 false
if (prevDeps === null) {
if (__DEV__) {
console.error(
"%s received a final argument during this render, but not during " +
"the previous render. Even though the final argument is optional, " +
"its type cannot change between renders.",
currentHookNameInDev
);
}
return false;
}
//在开发模式中,如果两个数组的长度不同,则输出一个错误消息。
if (__DEV__) {
// 比较依赖项数组的长度
if (nextDeps.length !== prevDeps.length) {
console.error(
"The final argument passed to %s changed size between renders. The " +
"order and size of this array must remain constant.\n\n" +
"Previous: %s\n" +
"Incoming: %s",
currentHookNameInDev,
`[${prevDeps.join(", ")}]`,
`[${nextDeps.join(", ")}]`
);
}
}
// 比较数组的元素
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 逐个比较数组的元素 如果发现不相等的元素 函数返回 false
// 这里的is函数等同于 Object.is的实现 它比较两个值是否相同
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
总结:
areHookInputsEqual函数是用来比较两个依赖数组——nextDeps和prevDeps是否相等:
在检查依赖项数组是否相等之前,函数首先进行了几项预检查。首先,它会在开发模式中检查是否启用了ignorePreviousDependencies标志。这可能是为了处理热重载的情况。如果该标志为真,函数立即返回false,表示这两个数组不相等。
接着,函数会检查prevDeps是否为null。如果为null,这意味着在前一个渲染过程中没有接收到最后的参数,但在这次渲染中接收到了。即使这最后的参数是可选的,它在渲染之间的类型也不能改变。这种情况下,函数会输出一个错误消息,并返回false。
在开发模式中,函数还会比较两个依赖项数组的长度。如果它们的长度不同,这通常意味着程序中存在错误。因为当组件在不同的渲染之间更改了依赖数组的大小时,React的hooks规则要求这个数组在每次渲染时都保持不变。如果发现数组大小不同,函数会输出一个相关的错误消息。
最后,函数逐个比较两个数组的元素。它使用了is函数(等同于是Object.is的实现)来确保两个值是严格相等的。如果所有的元素都相等,函数返回true。如果发现任何不相等的元素,函数就返回false。