详解react中useCallback内部是如何实现的

 更新时间:2023年07月04日 09:11:32   作者:AKclown  
前几天有人在问在useCallback函数如果第二个参数为空数组, 为什么拿不到最新的state值,那么这一章就来分析一下useCallback内部是如何实现的,感兴趣的小伙伴跟着小编一起来学习吧

示例demo与debug

新建了一个react项目,将APP.tsx改写成如下代码

import  { useCallback, useState } from 'react';
function App() {
  const [num, updateNum] = useState(0);
  const TestCallback  = useCallback(() =>{
    console.log('num: ', num);
  },[]);
  return (
    <div className="App">
        <p onClick={() => {
          updateNum(num => num + 1);
          updateNum(num => num + 1);
          updateNum(num => num + 1);
        }}>{num}</p>
        <p onClick={TestCallback}>打印</p>
    </div>
  );
}
export default App;

在浏览器的source设置断点,熟悉一遍useCallback的调用流程。(由于.gif过大,这里就不上git了,自行调试)

源码解析

useCallback的整体流程框架

在react中mount阶段和update阶段进入到同一个useCallback方法里。但resolveDispatcher找到的dispatch对象mountupdate会不同,最终导致在mount阶段调用mountCallback而update阶段调用的是updateCallback
下面为调用useCallback方法触发的行为

function useCallback(callback, deps) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

下面来看看resolveDispatcher是如何获取到dispatch的

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  ...
  return ((dispatcher: any): Dispatcher);
}

ReactCurrentDispatcher.current会在renderWithHooks方法中进行所处阶段判断并且赋值。如果current === null || current.memoizedState === null为true表示在mount阶段反正为update阶段

function renderWithHooks<Props, SecondArg>(...) {
     ...
     ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
}
// mount阶段调用的dispatch
const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
};
// update阶段调用的dispatch
const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useCallback: updateCallback,
};

从上面的代码分析可以知道在mounted阶段调用的是mountCallbackupdate阶段调用updateCallback

Hook

一个函数式组件链路: fiber(FunctionComponent) => Hook(保存数据状态) => Queue(更新的队列结构) => update(更新的数据)在后续需要使用到Hook这个结构,那么先来看一下Hook是数据结构是怎么样的,以及属性的作用是什么?

  • memoizedState 存放的是Hook对应的state
  • next链接到下一个Hook,从而形成一个无环单向链表
  • queue存储同一个hook更新的多个update对象,数据结构为环状单向链表
// 组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: hook,
  ...
};
const hook: Hook = {
    // 1.  memoizedState 存放的是Hook对应的state
    memoizedState: null,
    // 2. next链接到下一个Hook,从而形成一个`无环单向链表`
    queue: null,
    // 3. queue存储同一个hook更新的多个update对象,数据结构为`环状单向链表`  
    next: null,
    ...
};

fiber与Hooks的关系(懒得画图了,引用了Understanding the Closure Trap of React Hooks)

mount阶段

分析mountCallback的实现

  • 通过mountWorkInProgressHook获取到对应的Hook对象
  • 判断条件deps是否为undefined
  • 回调函数判断条件存入到hook.memoizedState
  • 返回传入的回调函数
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;
}

mountWorkInProgressHook的实现,创建初始化Hook对象,并且将该Hook对象保存在workInProgressHook链路中. workInProgressHook表示正在执行的hook

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

在组件render时,每当遇到下一个Hook,通过移动workInProgressHook的指针来获取到对应的HookPS: 只要每次组件renderuseState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象

// fiber.memoizedState标识第一个Hook
workInProgressHook = fiber.memoizedState;
// 在组件`render`时,遇到下一个hook时
workInProgressHook = workInProgressHook.next;
....

update阶段

分析updateCallback的实现

  • 通过updateWorkInProgressHook获取到当前的Hook对象
  • hook.memoizedState获取到上一次缓存的state。假设这是第一次update那么其值就是mount阶段保存的[callback, nextDeps]数据
  • 如果依赖条件不为空,使用areHookInputsEqual判断依赖项是否更改。只会遍历数组第一层数据比较不会做深层比较。如果依赖项没变化,返回原本缓存的callback
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;
}

依赖比较areHookInputsEqual的方法实现

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  ...
  // $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;
}

总结

在React中会使用闭包机制来处理上文的callback回调函数。当包含useCallback组件被渲染时,React 会为该特定渲染周期创建一个闭包。闭包是一个封装的作用域,其中包含渲染时位于作用域内的变量、函数和其他引用
因此deps我们传入的是空数组,其回调函数callback一直引用的状态始终是初始状态,无法获取最新状态。缓存的回调函数可以访问最初调用时范围内的状态和道具

插件推荐

阅读源码可以通过使用Bookmarks快速标记代码位置,实现快速条件

到此这篇关于详解react中useCallback内部是如何实现的的文章就介绍到这了,更多相关react useCallback内部实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 在React Native中添加自定义字体的方法详解

    在React Native中添加自定义字体的方法详解

    在这篇指南中,我们将探索使用 Google Fonts 在 React Native 应用中添加自定义字体的方法,字体是优秀用户体验的基石,使用定制字体可以为你的应用程序提供独特的身份,需要的朋友可以参考下
    2024-02-02
  • 详解React开发必不可少的eslint配置

    详解React开发必不可少的eslint配置

    本篇文章主要介绍了详解React开发必不可少的eslint配置,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • React 错误边界组件的处理

    React 错误边界组件的处理

    这篇文章主要介绍了React 错误边界组件的处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • React如何实现像Vue一样将css和js写在同一文件

    React如何实现像Vue一样将css和js写在同一文件

    这篇文章主要介绍了React如何实现像Vue一样将css和js写在同一文件问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • react echarts刷新不显示问题的解决方法

    react echarts刷新不显示问题的解决方法

    最近在写项目的时候遇到了一个问题,当编辑完代码后echarts图正常展示 , 可再次刷新页面 , echarts会消失,所以本文给大家介绍了react echarts刷新不显示问题原因和解决方法,需要的朋友可以参考下
    2024-02-02
  • 如何应用 SOLID 原则在 React 中整理代码之开闭原则

    如何应用 SOLID 原则在 React 中整理代码之开闭原则

    React 不是面向对象,但这些原则背后的主要思想可能是有帮助的,在本文中,我将尝试演示如何应用这些原则来编写更好的代码,对React SOLID原则开闭原则相关知识感兴趣的朋友一起看看吧
    2022-07-07
  • React Router v6路由懒加载的2种方式小结

    React Router v6路由懒加载的2种方式小结

    React Router v6 的路由懒加载有2种实现方式,1是使用react-router自带的 route.lazy,2是使用React自带的 React.lazy,下面我们就来看看它们的具体实现方法吧
    2024-04-04
  • React 中 setState 的异步操作案例详解

    React 中 setState 的异步操作案例详解

    这篇文章主要介绍了React中setState的异步操作案例详解,文章围绕主题展开详细的内容介绍,具有一点点参考价值,感兴趣的小伙伴可以参考一下
    2022-08-08
  • 浅谈React深度编程之受控组件与非受控组件

    浅谈React深度编程之受控组件与非受控组件

    这篇文章主要介绍了浅谈React深度编程之受控组件与非受控组件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12
  • 渐进式源码解析React更新流程驱动

    渐进式源码解析React更新流程驱动

    这篇文章主要为大家介绍了渐进式源码解析React更新流程驱动详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04

最新评论