【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。
相关推荐
GanGuaGua2 小时前
CSS:盒子模型
开发语言·前端·css·html
进取星辰2 小时前
23、Next.js:时空传送门——React 19 全栈框架
开发语言·javascript·react.js
GalenWu8 小时前
对象转换为 JSON 字符串(或反向解析)
前端·javascript·微信小程序·json
GUIQU.8 小时前
【Vue】微前端架构与Vue(qiankun、Micro-App)
前端·vue.js·架构
zwjapple8 小时前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
数据潜水员8 小时前
插槽、生命周期
前端·javascript·vue.js
2401_837088508 小时前
CSS vertical-align
前端·html
优雅永不过时·8 小时前
实现一个漂亮的Three.js 扫光地面 圆形贴图扫光
前端·javascript·智慧城市·three.js·贴图·shader
CodeCraft Studio9 小时前
报表控件stimulsoft教程:使用 JoinType 关系参数创建仪表盘
前端·ui
春天姐姐10 小时前
vue知识点总结 依赖注入 动态组件 异步加载
前端·javascript·vue.js