React Hooks 源码解读之 useCallback

388 阅读6分钟

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

react 版本:v17.0.3

1、Hook 入口

React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:

  • 挂载阶段:

    useCallback = ReactCurrentDispatcher.current.useCallback = HooksDispatcherOnMount.useCallback = mountCallback;

  • 更新阶段:

    useCallback = ReactCurrentDispatcher.current.useCallback = HooksDispatcherOnUpdate.useCallback = updateCallback;

因此,组件在挂载阶段,执行 useCallback,其实执行的是 mountCallback,而在更新阶段,则执行的是 updateCallback 。

2、挂载阶段

组件在挂载阶段,执行 useCallback,实际上执行的是 mountCallback,下面我们来看看 mountCallback 的实现。

2.1 mountCallback

// packages/react-reconciler/src/ReactFiberHooks.new.js

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
  const hook = mountWorkInProgressHook();
  // 根据 useCallback 的第二个参数初始化 依赖项数组
  const nextDeps = deps === undefined ? null : deps;
  // 将 useCallback 的第一个参数 和相应的依赖项保存到 hook 对象的 memoizedState 属性上
  hook.memoizedState = [callback, nextDeps];
  // 返回的是useCallback 的参数 callback 本身
  return callback;
}

组件在挂载时,执行 useCallback,首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中:

const hook = mountWorkInProgressHook();

接着根据 useCallback 的第二个参数 deps 来初始化需要保存起来的 依赖项数组:

const nextDeps = deps === undefined ? null : deps;

然后将 useCallback 的参数 callback 和相应的依赖项保存到 hook 对象的 memoizedState 属性上,并返回useCallback 的参数 callback:

hook.memoizedState = [callback, nextDeps];
// 返回的是useCallback 的参数 callback 本身
return callback;

mountCallback 和 mountMemo 的实现虽然几乎是一样的,但区别在于 useMemo 返回的是计算后的 memoized 值,而 useCallback 直接返回的是它的参数 callback 本身。

2.2 mountWorkInProgressHook

在 mountCallback 函数中,使用 mountWorkInProgressHook() 函数创建了一个新的 hook 对象,我们来看看它是如何被创建的:

// packages/react-reconciler/src/ReactFiberHooks.new.js

// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };
  
   // Hooks are stored as a linked list on the fiber's memoizedState field
  // 将 新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上

  // 只有在第一次打开页面的时候,workInProgressHook 为空
  if (workInProgressHook === null) {
    // This is the first hook in the list
    // 链表上的第一个 hook
    
    // currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,也同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性;如果 workInProgressHook 对象存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。

3、更新阶段

组件在更新阶段,执行 useCallback,实际上执行的是 updateCallback,下面我们来看看 updateCallback 的实现。

3.1 updateCallback

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 获取该 useCallback 对应的hook 对象
  const hook = updateWorkInProgressHook();
  // 根据 useCallback 的第二个参数计算新的 依赖
  const nextDeps = deps === undefined ? null : deps;
  // 之前的 回调函数和依赖项 [callback, nextDeps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 比较前后的依赖是否相同
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 前后的依赖相同,说明不需要重新保存 callback 和 deps,而是直接返回上一次的 callback
        return prevState[0];
      }
    }
  }

  // 前后的依赖数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会重新保存组件更新时重新创建的 callback 和 deps
  hook.memoizedState = [callback, nextDeps];
  // 直接useCallback 的参数 callback 本身
  return callback;
}

在更新阶段,执行 useCallback,首先会调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber 节点上的hook对象:

const hook = updateWorkInProgressHook();

接着获取新的依赖,方便与旧的依赖作比较,从而决定是否需要重新保存 useCallback的两个参数 callback 和 deps:

const nextDeps = deps === undefined ? null : deps;

然后从当前的 hook 对象中获取之前的依赖项数组,调用 areHookInputsEqual() 函数来比较前后的依赖项数组是否相同:

const prevState = hook.memoizedState;
if (prevState !== null) {
  if (nextDeps !== null) {
    // 比较前后的依赖是否相同
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 前后的依赖相同,说明不需要重新保存 callback 和 deps,而是直接返回上一次的 callback
      return prevState[0];
    }
  }
}

如果 areHookInputsEqual() 返回的结果为 true,说明前后的 依赖项数组 是一样的,说明不需要重新保存 useCallback的两个参数,而是直接返回上一次保存的 callback。

如果 areHookInputsEqual() 返回的结果为 false,则会执行下面的语句:

// 前后的依赖数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会重新保存组件更新时重新创建的 callback 和 deps
hook.memoizedState = [callback, nextDeps];
// 直接useCallback 的参数 callback 本身
return callback;

只要是前后的依赖项数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会执行上面的代码。因此,我们在使用 useCallback 的时候,应该传入依赖项数组,有助于避免非必要的渲染 。

3.2 updateWorkInProgressHook

在 updateCallback 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.

  // 获取 当前 hook 的下一个 hook
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // 取下一个 hook 为当前的hook
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    // 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook

    invariant(
      nextCurrentHook !== null,
      '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) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这里分两种情况:

  • 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
  • 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。

前后的 依赖项数组 是否一样,是通过 areHookInputsEqual() 来判断的,我们来看看它的实现。

3.3 areHookInputsEqual

// packages/react-reconciler/src/ReactFiberHooks.new.js

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
    
  // 删除了 dev 代码
    
  if (prevDeps === null) {
    
    // 删除了 dev 代码
    
    return false;
  }

  // 删除了 dev 代码
    
  // deps 是一个 Array,循环遍历去比较 array 中的每个 item
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // is比较函数是浅比较
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

在 areHookInputsEqual 函数中,循环遍历 deps ,调用 is 方法去比较依赖项数组中的每个依赖,值得注意的是,is 方法是浅比较,也就是说如果是深比较那一定会更新的。下面是 is 方法的源码:

// packages/shared/objectIs.js

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 =
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

Object.is() 方法判断两个值是否为同一个值,若当前浏览器支持该方法,则调用该方法来判断前后两个依赖项是否相同,若不支持,则调用React自己实现 is 方法来比较。

4、useCallback 流程图

5、总结

我们知道 useCallback 将会返回其参数 callback 的 memoized 版本,该 callback 仅在某个依赖项改变时才会更新。虽然 useCallback 可以对数据进行缓存,但是也不能因此而滥用,我们应该考虑哪些数据值得去缓存,因为对于 useCallback 来说,对比依赖变化是需要时间的,我们应该对此进行衡量,才能够更好的去用 hook,而不是为了优化而优化。