React Hooks 源码解读之useCallback

323 阅读4分钟

本文将简要介绍useCallback的基础用法,源代码实现。

1. 函数用途

const cachedFn = useCallback(fn, dependencies)
  1. fn:要缓存的函数值。React 将在初始渲染期间将您的函数返回(不是调用)。在下一次渲染时,如果自上次渲染以来依赖项没有更改,React 将提供相同的函数。否则提供当前渲染期间传递的函数,并将其存储起来以备以后重用。
  2. 依赖项:fn 代码内部引用的所有响应值的数组
  3. return: 在初始渲染中,useCallback 返回fn 函数,在后续渲染期间,返回上次渲染中已存储的 fn 函数(如果依赖项未更改),或者返回本次渲染期间传递的 fn 函数。

2. 源代码位置

该函数的源代码位于react包下的ReactHooks.js文件。代码如下:

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

代码讲解:

  1. 参数:该函数接受两个参数:回调函数callback和一个数组。
  2. 然后调用resolveDispatcher函数获得dispatcher

resolveDispatcher函数如下:

function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  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://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}

注意到 const dispatcher = ReactSharedInternals.H;说明dispatcher的值是从ReactSharedInternals中取得的。ReactSharedInternals是React内部定义的一个对象,其结构如下:

const ReactSharedInternals: SharedStateServer = ({
  H: null,
  A: null,
}: any);

3. renderWithHooks文件

  1. 那么ReactSharedInternals.H的值是多少呢。根据追踪ReactSharedInternals.H的值发现,最终useCallback的值有两个函数:分别是updateCallbackmountCallback
  2. ReactFiberHooks.js文件的renderWithHooks函数中,对useCallback的两种情况进行了如下赋值,分为mountCallbackupdateCallback
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // [Not Native Code]
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // [Not Native Code]
}

3.1 mountCallback函数实现

mountCallback函数如下:

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

可以看到先调用了mountWorkInProgressHook获得hook数据结构。

3.1.1 mountWorkInProgressHook函数实现

mountWorkInProgressHook函数如下:

function mountWorkInProgressHook(): Hook {
//首先创建了一个hook对象
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

解析:

  1. workInProgressHook是指向当前正在渲染的 hook 列表中某个 hook 的指针.
  2. if (workInProgressHook === null)说明是第一个要渲染的hook
  3. currentlyRenderingFiber.memoizedState = workInProgressHook = hook;让currentlyRenderingFiber.memoizedState指向第一个hook
  4. workInProgressHook = workInProgressHook.next = hook;如果不是第一个hook,就将其连接到hookList末尾
  5. 最后返回workInProgressHook指针,可以通过该指针接触并调用hooks。

3.1.2 hook.memoizedState作用解释

返回到mountCallback的实现,发现有一句hook.memoizedState = [callback, nextDeps];hook.memoizedState 在 React 的 hooks 实现中起着重要的作用,它用于存储当前 hook 的状态值。具体来说,它的用途包括:

  1. 状态存储: memoizedState 存储 hook 的最新状态值,确保在组件重新渲染时能够保持和访问这个状态。
  2. 状态持久化: 当组件重新渲染时,React 会根据 memoizedState 的值来决定如何更新组件的状态,而不必重新计算状态或重新初始化。 这也是useCallback避免重复渲染的关键

3.2 updateCallback的实现

更新时,目标是仅当一个依赖项发生更改时才为提供新的函数引用。

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

解析:

  1. const hook = updateWorkInProgressHook();创建或重用钩子数据结构对象。
  2. nextDeps = deps === undefined ? null : deps;推断依赖项数组,如果未提供任何内容,则返回 null。
  3. 当依赖项不为 null 时,这意味着正在记忆,将继续将它们与之前的依赖项进行比较areHookInputsEqual(nextDeps, prevDeps)
  4. 比较前一个和后一个依赖项,如果相同,则返回前一个值 return prevState[0];(memoizedState 数组中的第一个元素)。
  5. 当依赖项发生变化时,与 mountCallback 类似,将 [callback, nextDeps] 存储到钩子对象的 memoizedState 属性中。

areHookInputsEqual 函数用于所有使用依赖项数组的hooks。 当没有先前的依赖项时,总是返回 false,并且每次渲染都会刷新。如果有依赖项,循环两个数组并使用 Object.is 比较各个值。