【React 源码阅读】useCallback

背景

初入 React Hooks 的小伙伴可能比较疑惑,为什么 useCallback 这个 Hook 每次写一个都要传入相应的 deeps 呢?,简直不要太麻烦了。

源码阅读

前面一篇文章里提到的类似,useCallback 也是用链表来进行存储和和初始化的。

mountCallback

mount 阶段,会执行 mountCallback,它本身需要传入两个参数:

  • callback:实际需要执行的函数
  • deps: 函数执行时需要传入的依赖
typescript 复制代码
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;
}

可以看到,mountCallback 内还会执行 mountWorkInProgressHook 来生成链表,拿到的 hook 其实就是链表中的某个节点。 最终,会把我们传入的 callbackdeps 都绑定在 hook.memoizedState 上。

updateCallback

update 阶段,会执行 updateWorkInprogressHook 来更新链表,之后,会拿 nextDepsprevDeps 来进行对比:

typescript 复制代码
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  if (__DEV__) {
    if (ignorePreviousDependencies) {
      // Only true when this component is being hot reloaded.
      return 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__) {
    // Don't bother comparing lengths in prod because these arrays should be
    // passed inline.
    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(', ')}]`,
      );
    }
  }
  // $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;
}

代码很简单,其实就是对 prevDepsnextDeps 做了一层浅比较:

  • 相等,则返回之前缓存的 callback
  • 不相等,则返回当前定义的 callback

完整代码:

typescript 复制代码
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 (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

所以可以看出, useCallback 就是对函数做了一层缓存,deps 在比较时是做的浅比较。

总结

useCallback 为啥要传 deps

React Hook 的设计如此,为了减少 re-render 下 callback 的创建次数。

  • 优点:传入 deps 后,如果 render 前后两次的 deps 没有变化,那么会复用上一次创建的 callback,节省函数创建的开销,也是典型的空间换时间的做法。
  • 缺点:
    • deps 有心智负担,容易造成死循环。遇到 deps 死循环时,可以考虑用 useRef 来解决。
    • 所有 callback 都使用 useCallback 包一层,也会有额外的内存开销,毕竟 callback 会一直存在链表上的某个节点里

最佳实践

不要直接使用 useCallback,需要的时候再用它:

  • 不使用的场景:比如直接在 render 内使用的函数,其实就没太大必要包一层 useCallback
  • 使用的场景:比如在 useEffect 或者其他 hook 中用到了这个函数,就可以考虑用 useCallback 套一层。否则会因为每次渲染时 callback 都是一个新引用而导致重复执行 hook。
相关推荐
秋秋小事2 小时前
React Hooks useEffect的使用
react.js
胡gh5 小时前
页面卡成PPT?重排重绘惹的祸!依旧性能优化
前端·javascript·面试
言兴5 小时前
# 深度解析 ECharts:从零到一构建企业级数据可视化看板
前端·javascript·面试
山有木兮木有枝_6 小时前
TailWind CSS
前端·css·postcss
烛阴6 小时前
TypeScript 的“读心术”:让类型在代码中“流动”起来
前端·javascript·typescript
杨荧6 小时前
基于Python的农作物病虫害防治网站 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python
Moment7 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源
程序视点8 小时前
Escrcpy 3.0投屏控制软件使用教程:无线/有线连接+虚拟显示功能详解
前端·后端
silent_missile8 小时前
element-plus穿梭框transfer的调整
前端·javascript·vue.js
专注VB编程开发20年8 小时前
OpenXml、NPOI、EPPlus、Spire.Office组件对EXCEL ole对象附件的支持
前端·.net·excel·spire.office·npoi·openxml·spire.excel