[React源码解析] useMemo

97 阅读2分钟

useMemo分为两个阶段,一个是mount阶段,另外一个是update阶段;

mountMemo

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
  1. 组件第一次渲染的时候执行useMemo传入的第一个函数参数;
  2. 然后将执行后的值和useMemo传入的第二个依赖参数保存到当前hook的memoizeState中;
  3. 然后返回useMemo的第一个函数参数的执行结果;

updateMemo

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
  1. 从当前hook中拿到上一次执行的结果;
  2. 然后就去判断nextDeps跟preDeps值是否相等,如果相等就直接返回上一次的值;
  3. 否者重新执行useMemo的第一函数入参;
  4. 然后将新的执行结果和依赖更新到hook中,并返回执行结果;

使用场景

  1. 如果组件中需要用到一个值需要时间复杂度比较高的计算得到结果的;
  2. 组件频繁渲染,而有些值的计算只依赖其中某一两个变量,则可以通过useMemo来跳过频繁的计算;

更多使用场景zh-hans.react.dev/reference/r…

可能的忧虑

  1. Fiber的hooks链中会多一个useMemo的hook来存储上一次的执行结果和依赖,会增加内存的开销和hook遍历的时间复杂度;

扩展

  1. 注意这个,说明有些场景下我们传入给useMemo的函数可能会执行两次;其中原因参考zh-hans.react.dev/reference/r…
if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
}
  1. 怎么去比较deps相等的
if (areHookInputsEqual(nextDeps, prevDeps)) {
    return prevState[0];
}

遍历然后用Object.is判断

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  ```
  一些不重要的代码
  ```
  // $FlowFixMe[incompatible-use] found when upgrading Flow
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  // $FlowFixMe[method-unbinding]
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;