【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。
相关推荐
怎么就重名了几秒前
Kivy的属性系统
java·前端·数据库
hxjhnct30 分钟前
JavaScript Promise 的常用API
开发语言·前端·javascript
web小白成长日记39 分钟前
前端让我明显感受到了信息闭塞的恐怖......
前端·javascript·css·react.js·前端框架·html
GIS之路1 小时前
GDAL 实现创建几何对象
前端
liulilittle1 小时前
CLANG 交叉编译
linux·服务器·开发语言·前端·c++
自信阿杜2 小时前
跨标签页数据同步完全指南:如何选择最优通信方案
前端·javascript
牛马1112 小时前
WidgetsFlutterBinding.ensureInitialized()在 Flutter Web 端启动流程的影响
java·前端·flutter
Captaincc2 小时前
2025: The year in LLMs
前端·vibecoding
指尖跳动的光2 小时前
Vue的nextTick()方法
前端·javascript·vue.js
码事漫谈2 小时前
可能,AI早都觉醒了
前端