useCallback useMomo能够达到性能优化效果源码解析(version 18.0)

inno

inno

Posted on February 21, 2023

useCallback useMomo能够达到性能优化效果源码解析(version 18.0)

question

为什么使用useCallback 和useMemo返回的memoized值可以起到性能优化的作用。

源码分析

  • 我们在packages\react\src\ReactHooks.js可以找到对应的hook
export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}
Enter fullscreen mode Exit fullscreen mode
  • 在这里我们可以看到其useCallback,useMemo是调用dispatcher对应的方法, 而dispatcher是由resolveDispatcher()生成。而resolveDispatcher()中的dispatcher 是由ReactCurrentDispatcher.current生成。因此我们需要查看ReactCurrentDispatcher.current的数据是在哪里挂载的。
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
//如果在渲染阶段之外访问,将导致空访问错误。我们
  //故意不抛出我们自己的错误,因为这是在热路径中。
  //也有助于确保这是内联的。
  return ((dispatcher: any): Dispatcher);
}

Enter fullscreen mode Exit fullscreen mode
  • 在packages\react-reconciler\src\ReactFiberHooks.js中的renderWithHooks对ReactCurrentDispatcher.current进行数据挂载这里只截取对应代码
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;


  //TODO 如果挂载期间根本没有使用挂钩,则发出警告,然后在更新期间使用一些挂钩。
  //目前我们将更新渲染识别为一个装载,因为 memoizedState === null。
//这很棘手,因为它对某些类型的组件有效(例如 React.lazy)

  //使用 memoizedState 来区分挂载/更新只有在至少使用一个有状态钩子的情况下才有效。
  //非状态挂钩(例如上下文)不会添加到 memoizedState,
  //所以 memoizedState 在更新和挂载期间将为 null。

    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 因此当dispatcher.useCallback和dispatcher.useMemo实际调用的是会根据是否是初次渲染调用对应的HooksDispatcherOnMount和HooksDispatcherOnUpdate
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
};
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
};
Enter fullscreen mode Exit fullscreen mode
  • HooksDispatcherOnMount和HooksDispatcherOnUpdate调用对应的方法,这里可以看到useCallback和useMemo会都会被hook.memoizedState缓存,区别就一个缓存的是函数,一个缓存的是值。对 hook.memoizedState查看我们可以了解到mountWorkInProgressHook和updateWorkInProgressHook生成,

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

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;
}

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;
}

Enter fullscreen mode Exit fullscreen mode
  • mountWorkInProgressHook workInProgressHook = workInProgressHook.next = hook 根据这段代码我们可以了解到一个函数组件的所有hook函数会形成一个hooks链表,而workInProgressHook 指针会指向最后一个节点。这个hooks链表是存放在currentlyRenderingFiber.memoizedState

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
   //这是列表中的第一个钩子
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
   //追加到列表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

Enter fullscreen mode Exit fullscreen mode
  • updateWorkInProgressHook workInProgress 树中的 fiber 节点的下一个 hook 存在,链表直接指向节点,否则就从对应的 current 的 fiber 节点克隆过来,然后把这些 hook 构建出新的链表放到 currentlyRenderingFiber.memoizedState 上,方便下次更新时使用;
function updateWorkInProgressHook(): Hook {
//此函数用于更新和由 a 触发的重新渲染
  //渲染阶段更新。它假设有一个当前的钩子我们可以
  //克隆,或者我们可以从之前的渲染过程中进行的工作钩子
  //用作基础。当我们到达基本列表的末尾时,我们必须切换到
  //用于挂载的调度程序。
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
   //已经有一个正在进行的工作。重复使用它。
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    //从当前钩子克隆。

    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        //这是初始渲染。当组件到达此分支
        //挂起,恢复,然后呈现一个额外的钩子。
        const newHook: Hook = {
          memoizedState: null,

          baseState: null,
          baseQueue: null,
          queue: null,

          next: null,
        };
        nextCurrentHook = newHook;
      } else {
        // This is an update. We should always have a current hook.
        throw new Error('Rendered more hooks than during the previous render.');
      }
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
     //这是列表中的第一个钩子。
    // 若这是链表的第一个hook节点,则使用 currentlyRenderingFiber.memoizedState 指针指向到该hook
    // currentlyRenderingFiber 是在 renderWithHooks() 中赋值的,是当前函数组件对应的fiber节点
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
   //附加到列表的末尾。
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}
Enter fullscreen mode Exit fullscreen mode

总结

之所以useCallback和useMemo可以达到性能优化的效果,是因为我们在初始节点已经把所有的 hooks 都挂载在链表中了,在更新阶段,所有的 hooks 都会进入到 update 阶段,比如 useState()内部会执行 updateState(),useEffect()内部会执行 updateEffect()等。二这些 hooks 的 update 阶段执行的函数里,都会执行函数 updateWorkInProgressHook()。

updateWorkInProgressHook()函数的作用,就是从 hooks 的链表中获取到当前位置,上次渲染后和本次将要渲染的两个 hook 节点:

currentHook: current 树中的那个 hook;即当前正在使用的那个 hook;

workInProgressHook: workInProgress 树中的那个 hook,即将要执行的 hook;
当通过对比两个hook,当依赖项没有发生改变时,workInProgressHook链表的指针会直接指向之前的fiber节点的地址。而不会重新创建。因此达到性能优化的效果

Image description

💖 💪 🙅 🚩
inno
inno

Posted on February 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related