【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。
相关推荐
Jave210811 分钟前
实现全局自定义loading指令
前端·vue.js
奔跑的呱呱牛15 分钟前
CSS Grid 布局参数详解(超细化版)+ 中文注释 Demo
前端·css·grid
木斯佳38 分钟前
前端八股文面经大全:影刀AI前端一面(2026-04-01)·面经深度解析
前端·人工智能·沙箱·tool·ai面经
小江的记录本1 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
无人机9012 小时前
Delphi 网络编程实战:TIdTCPClient 与 TIdTCPServer 类深度解析
java·开发语言·前端
lUie INGA2 小时前
rust web框架actix和axum比较
前端·人工智能·rust
OPHKVPS3 小时前
VoidStealer新型窃密攻击:首例利用硬件断点绕过Chrome ABE防护,精准窃取v20_master_key
前端·chrome
gechunlian883 小时前
SpringBoot3+Springdoc:v3api-docs可以访问,html无法访问的解决方法
前端·html
驾驭人生4 小时前
ASP.NET Core 实现 SSE 服务器推送|生产级实战教程(含跨域 / Nginx / 前端完整代码)
服务器·前端·nginx
酉鬼女又兒4 小时前
零基础快速入门前端ES6 核心特性详解:Set 数据结构与对象增强写法(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6