# React 的源码与原理解读(十一):hooks 的原理

news2024/11/22 15:56:38

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

这个章节和前面的部分相对独立,我们将来讲一下我们在 React 16.8 更新的新特性 Hooks,我们都知道 Hooks 的提出主要就是为了解决函数组件的状态问题而存在的,之前我们的 class 组件有状态,但是 function 组件没有,所以为了解决这个问题,hooks 诞生了。那么为了让我们的 hooks 挂在在整个 React 的生命周期中, React 是怎么样处理他们的呢,我们来看看这部分的内容:

Hooks 的定义

首先我们来看看hooks 的代码,他们存放在 /react/blob/main/packages/react/src/ReactHooks.js 这个位置,他们都使用 resolveDispatcher初始化了一个 dispatcher ,然后调用 dispatcher 对应的方法来实现他们的逻辑,而 resolveDispatcher 中的 dispatcher 又是从 ReactCurrentDispatcher.current 获取的:

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

export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  if (__DEV__) {
  //....省略
  }
  return dispatcher.useContext(Context);
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

回到 renderWithHooks

关于 ReactCurrentDispatcher.current ,我们需要回到我们第一次出现 hooks 相关操作的位置,也就是我们的 renderWithHooks 函数,当时我们省略了相关的内容,现在我们需要回过来讲这部分的内容:

首先我们看到 renderWithHooks 函数的开始部分,我们根据 current === null 的判断来决定我们是使用初始化的 hooksDispatcher 还是更新的 hooksDispatcher,这两个 hooks 就是我们的刚刚看到的在函数中调用的 Dispatcher ,其中每一项都包含了我们用到的各种 hooks 的操作函数

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // 获取当前函数组件对应的 fiber,并且初始化
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;


  if (__DEV__) {
    // ....
  } else {
    // 根据当前阶段决定是初始化hook,还是更新hook
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  // 调用函数组件
  let children = Component(props, secondArg);
    
  // 重置hook链表指针
  currentHook = null;
  workInProgressHook = null;
    
  return children;
}

const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

初始化时挂载 hooks

之后我们根据我们刚刚的两个阶段,Mount 和 Update 分别来看看我们的代码逻辑,首先是 Mount 时挂载我们的 hooks,通过查看每个HooksDispatcherOnMount 中的函数,我们可以看到,他们都使用了一个叫 mountWorkInProgressHook 的函数,我们来看看这个函数的逻辑:

  • 首先它创建了一个 hook ,它的数据结构在注释已经写清楚了,每种 hooks 需要保存不一样的值,这个我们之后会详细来说,同时还会存放我们 hooks 的更新队列,这和我们上一篇的 updateQueue 是一致的,不再赘述

  • workInProgressHook 是这个函数组件中的 hooks 的链表,一个函数组件中的 hooks 的是以链表的形式保存在一起的,而这个链表会被保存在 currentlyRenderingFibermemoizedState中,这个 currentlyRenderingFiber 则是我们之前获取到的当前函数组件对应的 Fiber

function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  ...
}
  
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null, // 上次渲染时所用的 state
    baseState: null,  // 已处理的 update 计算出的 state
    baseQueue: null,  // 未处理的 update 队列(上一轮没有处理完成的)
    queue: null,      // 当前的 update 队列
    next: null, // 指向下一个hook
  };

  // 保存到链表中
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

更新时的 hooks

看完了挂载时的 hooks ,我们再来看看更新,我们还是只看公共的部分:

  • 首先我们知道,我们 React 是有两棵树的,一颗是 Current 树表示当前展示的树,一颗是 WorkInProgress 树表示当前的构建的树,那么我们的 hooks 链表也因此分为两个 currentHook 和 workInProgressHook
  • 现在当我们要更新一个 hooks 的时候,我们首先使用 nextCurrentHook 和 nextWorkInProgressHook 来标识下一个需要操作的 hooks 。如果 nextWorkInProgressHook 存在,我们直接使用它即可,否则,我们需要从我们的 nextCurrentHook 处克隆一份我们的 hook 放入其中
function updateWorkInProgressHook(): Hook {

  // 获取 current 树上的 Fiber 的 hook 链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // workInProgress 树上的 Fiber 的 hook 链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
    
  // 如果 nextWorkInProgressHook 不为空,直接使用
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    //否则我们克隆一份 hooks
    currentHook = nextCurrentHook;
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

总结拓展 & 经典面试题

根据上面的函数我们也可以看到,当我们更新一个 hooks 的时候,我们需要遍历它的 Current 树和 WorkInProgress 树的 hooks 链表,所以它的两个链表标识的 hooks 顺序必须一致,这也引申出了一个我们常见的面试题 —— 为什么需要将hook函数放在函数组件的顶层

我们试想这样一种情况:如果有一个 hook 在循环条件或者 if 语句中产生,当我们挂载时和我们更新时,两次的执行环境不同导致了,导致了两次产生的 hook 不同或者顺序不同,但是我们还是按照我们预想的顺序对我们的两个链表进行遍历,最后导致的结果就是,我们错误的复用了另一个 hook 作为我们当前需要复用的 hook ,从而产生了不可预计的结果,一个例子如下:

我们设置两个 useState 我们让我们的第一个 hook 在执行一次后不再执行

let isMount = false;
if(!isMount) {
    const [num, setNum] = useState('value1'); // useState1
    isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2

那么我们在第一次调用 ,也就是 mount 的时候得到的链表是 : useState1 – useState2

显然我们在更新的时候的链表应该是 useState2

但是根据我们上文的代码,我们在更新的时候,需要复用一个 hook 作为我们的 useState2,此时我们的 nextCurrentHook 指向的是链表的第一个 hook 也就是 useState1

最后整个逻辑就乱套了,所以我们要牢记 需要将hook函数放在函数组件的顶层,不能在循环、条件或嵌套函数中调用 Hooks

hooks 的使用和后续

我们现在已经知道了我们 hooks 的逻辑,之后的几篇里面,我希望可以讲解一些常用的 hooks 的原理和源码的解析,大致包括:

  • 最常用的 useStateuseEffect
  • 性能优化 useMemouseCallback
  • 获取元素 useRef

可能还包括一些其他的和自定义 hooks 的内容,大家可以持续关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/503372.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

triton 疑难手册

config.pbtxt 配置参数手册 backend或platform参数用于指示nvidia triton用对应的backend加载模型参数&#xff0c;它的使用示例如下&#xff1a; name: "xxx" platform: "pytorch_libtorch"max_batch_size: 8 input [ {name: "input0"data_ty…

算法加密与解密、字符编码与字符集

加密算法 加密保证数据不会被窃取或者修改 可逆和不可逆加密 区分在于加密后的结果是否可以还原 可逆加密&#xff1a;安全传输数据时使用(如jwt中的数据) AES:流加密 DES&#xff1a;块加密 RSA HS256 不可逆加密&#xff1a;同一个文件或内容每次加密的结果一…

GL绘制自定义线条2_手写曲线应用贝塞尔曲线

上一篇文章的曲线是由触摸点直接生成的&#xff0c;但触摸点并非连续的&#xff0c;而是离散的&#xff0c;而且屏幕触摸点采样的间隔时间其实不短&#xff0c;因此如果单纯只用触摸点生成OpenGL触摸曲线&#xff0c;在高速书写时会导致曲线看起来就像多个线段合起来一样&#…

为何ChatGPT一出现让巨头们都坐不住?

近几个月来&#xff0c;ChatGPT都是当仁不让的舆论话题。 上一次AI在全球范围内引起轰动&#xff0c;还是谷歌的AI机器人AlphaGO下棋战胜围棋世界冠军的时候。 ChatGPT的出现&#xff0c;让国内外几乎所有的科技巨头都坐立不安。 2月1日&#xff0c;谷歌母公司Alphabet首席执…

C++跨平台“点绘机” 代码解读

前言 球球大作战可以自定义皮肤&#xff0c;用画刷绘制。 想着用软件来绘制。 初次尝试&#xff0c;没有达成最终目的&#xff0c;不过也有很大收获。 仓库链接&#xff1a;https://github.com/sixsixQAQ/dolphin 问题 这个半成品&#xff0c;已经有了基本结构了&#xff…

高德地图api 地理编码(地址-->坐标)geocoder.getLocation在官方可以测试出结果,下载代码到本地却用不了 问题解决

问题 高德地图api 地理编码&#xff08;地址-->坐标&#xff09;功能&#xff0c;通过输入 地址信息 得到 经纬度信息。geocoder.getLocation在官方可以测试出结果&#xff0c;下载代码到本地却用不了。 官方示例测试&#xff0c;可以从地址得到坐标 下载官方代码本地运行却…

常见注意力机制解析

1.Squeeze-and-Excitation&#xff08;SE&#xff09; SE的主要思想是通过对输入特征进行压缩和激励&#xff0c;来提高模型的表现能力。具体来说&#xff0c;SE注意力机制包括两个步骤&#xff1a;Squeeze和Excitation。在Squeeze步骤中&#xff0c;通过全局平均池化操作将输…

【2023年Mathorcup杯数学建模竞赛C题】电商物流网络包裹应急调运与结构优化--完整作品分享

1.问题背景 2.论文摘要 为了应对电商物流网络中物流场地和线路电商物流网络中物流场地和线路上货量波动的情况&#xff0c; 设计合理的物流网络调整方案以保障物流网络的正常运行。本文运用 0-1 整数规划模型&#xff0c;多目标动 态规划模型&#xff0c;给出了问题的结果。 针…

深入讲解eMMC简介

1 eMMC是什么 eMMC是embedded MultiMediaCard的简称&#xff0c;即嵌入式多媒体卡,是一种闪存卡的标准&#xff0c;它定义了基于嵌入式多媒体卡的存储系统的物理架构和访问接口及协议&#xff0c;具体由电子设备工程联合委员会JEDEC订立和发布。它是对MMC的一个拓展&#xff0…

redi缓存使用

1、缓存的特征 第一个特征&#xff1a;在一个层次化的系统中&#xff0c;缓存一定是一个快速子系统&#xff0c;数据存在缓存中时&#xff0c;能避免每次从慢速子系统中存取数据。 第二个特征&#xff1a;缓存系统的容量大小总是小于后端慢速系统的&#xff0c;不可能把所有数…

GAMES101 计算机图形学 | 学习笔记 (上)

目录 环境安装什么是计算机图形学物体上点的坐标变换顺序齐次坐标光栅化如何判定一个点在三角形内光栅化填充三角形示例代码光栅化产生的问题 采样不足&#xff08;欠采样&#xff09;导致锯齿抗锯齿滤波算法 环境安装 1. C中安装opencv库 2. C中安装eigen库 3. C中安装open…

ChatGPT调教指北,技巧就是效率!

技巧就是效率 很多人都知道ChatGPT很火很强&#xff0c;几乎无所不能&#xff0c;但跨越了重重门槛之才有机会使用的时候却有些迷茫&#xff0c;一时间不知道如何使用它。如果你就是把他当作一个普通的智能助手来看待&#xff0c;那与小爱同学有什么区别&#xff1f;甚至还差劲…

热乎的面经——踏石留印

⭐️前言⭐️ 本篇文章记录博主面试北京某公司所记录的面经&#xff0c;希望能给各位带来帮助。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论…

Origin如何绘制三维图形?

文章目录 0.引言1.使用矩阵簿窗口2.三维数据转换3.三维绘图4.三维曲面图5.三维XYY图6.三维符号、条状、矢量图7.等高线图 0.引言 因科研等多场景需要&#xff0c;绘制专业的图表&#xff0c;笔者对Origin进行了学习&#xff0c;本文通过《Origin 2022科学绘图与数据》及其配套素…

63.空白和视觉层级的实战应用

例如看我们之前的小网页&#xff1b; 这些标题的上下距离一样&#xff0c;这样让我们很容易对这些标题进行混淆&#xff0c;我们可以适当的添加一点空白 header, section {margin-bottom: 96px; }这样看上去似乎就好很多&#xff01; 除此之外&#xff0c;如我们之间学的空…

【line features】线特征

使用BinaryDescriptor接口提取线条并将其存储在KeyLine对象中&#xff0c;使用相同的接口计算每个提取线条的描述符&#xff0c;使用BinaryDescriptorMatcher确定从不同图像获得的描述符之间的匹配。 opencv提供接口实现 线提取和描述符计算 下面的代码片段展示了如何从图像中…

K8S相关核心概念

个人笔记&#xff1a; 要弄明白k8s的细节&#xff0c;需要知道k8s是个什么东西。它的主要功能&#xff0c;就是容器的调度--也就是把部署实例&#xff0c;根据整体资源的使用状况&#xff0c;部署到任何地方 注意任何这两个字&#xff0c;预示着你并不能够通过常规的IP、端口…

如何全面学习Object-C语言的语法知识 (Xmind Copilot生成)

网址&#xff1a;https://xmind.ai/login/ 登录后直接输入&#xff1a;如何全面学习Object-C语言的语法知识&#xff0c;就可以生成大纲 点击右上角的 按钮&#xff0c;可以显示md格式的问题&#xff0c;再点击生成全文&#xff0c;就可以生成所有内容了&#xff0c; 还有这个…

CentOS7/8 安装 5+ 以上的Linux kernel

CentOS以稳定著称&#xff0c;稳定在另外一方面就是保守。所以CentOS7还在用3.10&#xff0c;CentOS8也才是4.18。而当前最新的Linux Kernel都更新到6.0 rc3了。其他较新的发行版都用上了5.10的版本。本文简单介绍如何在CentOS7、8上直接安装5.1以上版本的第三方内核。 使用ted…

5.8晚间黄金行情走势分析及短线交易策略

近期有哪些消息面影响黄金走势&#xff1f;本周黄金多空该如何研判&#xff1f; ​黄金消息面解析&#xff1a;周一亚洲时段&#xff0c;现货黄金小幅反弹&#xff0c;目前交投于2024.3美元/盎司附近&#xff0c;一方面是金价上周五守住了 2000 整数关口&#xff0c;逢低买盘涌…