五分钟搞懂 React Hooks 工作原理

2023-10-26

前言


Hooks 是 React 中比较火的一个概念, 用过的小伙伴都说好。

但是, 在使用 Hooks 的时候,我们可能会有很多疑惑:

1. 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?

2. 为什么只能在函数的最外层调用 Hook,不能在循环、条件判断或者子函数中调用?

3. 自定义的 Hook 是如何影响使用它的函数组件的?

4. Capture Value 特性是如何产生的?

今天这篇文章, 不会讲解 Hooks 的概念和用法,而是会带你从零实现一个 tiny hooks,知其然知其所以然。

useState

最简单的 useState 用法是这样的:

function Counter() {
  var [count, setCount] = useState(0);


  return (
    <div>
      <div>{count}</div>
      <Button onClick={() => { setCount(count + 1); }}>
        点击
      </Button>
    </div>
  );
}

基于 useState 的用法, 我们尝试着自己实现一个 useState

实现一个 useState:

function useState(initialValue) {
  var state = initialValue;
  function setState(newState) {
    state = newState;
    render();
  }
  return [state, setState];
}

这时我们发现,点击 Button 的时候,count 并不会变化

为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。

自然我们就能想到,把 state 提取出来,存在 useState 外面

var _state; // 把 state 存储在外面


function useState(initialValue) {
  _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
  function setState(newState) {
    _state = newState;
    render();
  }
  return [_state, setState];
}

到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。

接下来,让我们看看 useEffect 是怎么实现的。

useEffect

useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:

 useEffect(() => {
    console.log(count);
 }, [count]);

我们知道 useEffect 有几个特点:

1. 有两个参数 callback 和 dependencies 数组

2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行

3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行

实现一个 useEffect

let _deps; // _deps 记录 useEffect 上一次的 依赖


function useEffect(callback, depArray) {
  const hasNoDeps = !depArray; // 如果 dependencies 不存在
  const hasChangedDeps = _deps
    ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
    : true;
  /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  if (hasNoDeps || hasChangedDeps) {
    callback();
    _deps = depArray;
  }
}

到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。

此时我们应该可以解答一个问题:

为什么第二个参数是空数组,相当于 componentDidMount ?

因为依赖一直不变化,callback 不会二次执行。

Not Magic, just Arrays

到现在为止,我们已经实现了可以工作的 useState 和 useEffect。

但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。

比如

const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。

我们需要可以存储多个 _state 和 _deps。

如 《React Hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。

代码关键在于:

初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。

更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。

如果还是不清楚,可以看下面的图。

let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标


function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] 
    || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; 
  // 返回当前 state,并把 cursor 加 1
}


function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  
  cursor++;
}

我们用图来描述 memoizedState 及 cursor 变化的过程。

1. 初始化


2. 初次渲染

3. 事件触发

4. Re-Render

到这里,我们实现了一个可以任意复用的 useState 和 useEffect。

同时,也可以解答几个问题:

Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用?

A:memoizedState 数组是按hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到

Q:自定义的 Hook 是如何影响使用它的函数组件的?

A:共享同一个 memoizedState,共享同一个顺序。

Q:"Capture Value" 特性是如何产生的?

A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

真正的 React 实现

虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

React 中是通过类似单链表的形式来代替数组的。

源码地址:https://github.com/facebook/react/blob/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js#L336

通过 next 按顺序串联所有的 hook。

https://github.com/facebook/react/blob/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js#L477

type Hooks = {
  memoizedState: any, // 指向当前渲染节点 Fiber
  baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
  baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  queue: UpdateQueue<any> | null,// UpdateQueue 通过
  next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
 
type Effect = {
  tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
  create: () => mixed, // 初始化 callback
  destroy: (() => mixed) | null, // 卸载 callback
  deps: Array<mixed> | null,
  next: Effect, // 同上
};

memoizedState,cursor 是存在哪里的?

如何和每个函数组件一一对应的?

我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件。

hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

以上就是全部内容, 希望对大家有所启发。

最后

  • 欢迎扫码加我微信,拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

在看点这里

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

五分钟搞懂 React Hooks 工作原理 的相关文章

  • Ubuntu安装软件步骤

    Ubuntu安装软件步骤 sudo apt get update sudo apt get install flex bison gperfbuild essential curl zlib1g dev g multilib g 4 4 m
  • Source Insight 4.0 下载 安装 配置

    目录 下载地址 安装 打开 试用 导入工程 代码 1 新建一个项目 project 2 填充项目名及代码路径 3 这个直接点OK 4 导入项目文件 5 重建一下项目 6 打开项目文件 project Files 修改source insig
  • CS162 13-17 虚拟内存

    起源 为啥我们需要虚拟内存 需求是啥 可以给程序提供一个统一的视图 比如多个程序运行同一个代码段的话 同一个kernel 就可以直接共享 cpu眼里的虚拟内存 无限内存的假象 设计迭代过程 为啥这样设计 一个迭代过程 用上下界来做 缺点 还
  • Basic Level 1065 单身狗 (25分)

    题目 单身狗 是中文对于单身人士的一种爱称 本题请你从上万人的大型派对中找出落单的客人 以便给予特殊关爱 输入格式 输入第一行给出一个正整数 N 50 000 是已知夫妻 伴侣的对数 随后 N 行 每行给出一对夫妻 伴侣 为方便起见 每人对
  • cv2.error: OpenCV(4.6.0) :-1: error: (-5:Bad argument) in function ‘seamlessClone‘

    Can t parse p Sequence item with index 0 has a wrong type 1 软件环境 2 问题描述 3 解决方法 4 结果预览 1 软件环境 Windows10 教育版64位 Python 3 6
  • 函数式接口

    接口 package cn dali5 code01 函数式接口 有且仅有一个抽象方法的接口 可以有其他的方法 默认 静态 私有 函数式接口 适用于函数式编程场景的接口 Java中函数式编程的提现就是lambda表达式 所以函数式接口就是可
  • python子类定义报错:TypeError: __init__() missing 1 required positional argument: ‘prilege‘

    在学习 Python编程 从入门到实践 中类这一章节 其中子类的案例代码如下 class Car snip class Battery 一次模拟电动汽车电瓶的简单尝试 def init self battery size 70 初始化电瓶的
  • html5media使用api,html5中media(播放器)的api使用指南.pdf

    代码如下 HTML Audio API HTML5 Audio API HTML5 Audio API demo by target blank gt LearnShare Last update 2013 04 23 20 40 00 a
  • Python多线程、多进程和协程的实例讲解

    线程 进程和协程是什么 线程 进程和协程的详细概念解释和原理剖析不是本文的重点 本文重点讲述在Python中怎样实际使用这三种东西 参考 进程 线程 协程之概念理解 进程 Process 是计算机中的程序关于某数据集合上的一次运行活动 是系
  • WebUploader使用

    WebUploader用于文件的上传 文件上传过程为 网页中点击上传按钮 弹出选择文件窗口 并选择一个文件 在网页中显示选中的内容 给使用者一个反馈 点击上传按钮 文件开始上传 同时服务端开始接收文件 对于服务端而言 框架往往都有自己的接收
  • Jmeter(二十六) - 从入门到精通 - 搭建开源论坛JForum(详解教程)

    1 简介 今天这篇文章主要是给大家讲解一下 如何部署测试环境 这里宏哥部署一个开源测论坛 后边的文章中会用到这个论坛 并且也看到童鞋们在群里讨论如何在开发将测试包发给你以后 你如何快速地部署测试环境 这里就是简单的演示一下 应该具体项目灵活
  • 洛谷 P1085 不高兴的津津

    这个题目需要连续换行输入7组数据 并且对数据的最大值进行比较和提取 题目描述 津津上初中了 妈妈认为津津应该更加用功学习 所以津津除了上学之外 还要参加妈妈为她报名的各科复习班 另外每周妈妈还会送她去学习朗诵 舞蹈和钢琴 但是津津如果一天上
  • IDEA的配置JDK,Tomcat,Maven

    IDEA的配置JDK Tomcat Maven 先下载安装jdk 其中JDK为安装版 tomcat 和maven为非安装版 JDK安装完成后要设置3个坏境变量 tomcat和maven好像不设置也行 就下载下来解压就行了 maven最好还是
  • 小米android11账号补丁,小米10 MIUI11 解账户锁 可登小米账号 永不反锁 完美ROOT 解锁包...

    MIUI全机型有锁机账户锁刷机包 仅针对于有锁机用户使用 帮助已经购买到有锁机的用户 ROM版权归小米 官方所有 本人未持有任何版权 仅以分享形式发布 对ROM稳定性也不能做任何保证 如果你希望更好的系统 体验 我们非常建议购买正规渠道的小
  • 用Construct2开发一个小游戏(进阶)

    策划并用Construct2开发一个小游戏 进阶 游戏策划 楔子 Setting 公元2500年 与地球建交长达200之久的达克星球 Dark Star 单方面撕毁友好合约 对地球发起了进攻 面对源源不断的独眼怪大军 你踏入自己发明的 洋芋
  • MATLAB——读取多文件夹内文件并绘制图形(1)——逐行读取txt文件内字符串

    目录 1 添加路径 2 准备好图片名称和路径名称 3 读取txt文件中的字符串 1 添加路径 如果m文件和要读取的文件不在同一个路径下 需要借助下方代码将当前文件夹下的所有文件都包含进搜索路径中 addpath genpath F SaCo
  • Swin-Transformer

    原视频链接 https www bilibili com video BV1pL4y1v7jC spm id from 333 788 vd source f04f16dd6fd058b8328c67a3e064abd5 参考博文 2021
  • 哈夫曼编码设计(C)

    文章目录 前言 哈夫曼编码设计 总结 前言 大二 刚刚开始学数据结构与算法 写得不好 哈夫曼编码设计 现要求输入8个字符 a b c d e f g h 对应的权值 大于0的整数 然后设计哈夫曼编码实现输入对应8个字符组成的一串字符 字符串
  • centos 网络连接设置

    这里使用虚拟机 VirtualBox 来安装CentOS 6 3 32bit服务器版本 没有安装桌面 作为演示 所以全是命令操作 如何安装CentOS操作系统就不用我说的 虚拟机网络设置为桥接模式 Bridge 单独分配ip 不共享主机ip
  • 致命错误:Rdefines.h:没有那个文件或目录

    致命错误 Rdefines h 没有那个文件或目录 Rdefines h No such file or directory 关键词 CentOS 7 安装rpy2 pip3 install rpy2报错 python3 setup py

随机推荐

  • C#——字符串

    System String类 1 创建字符串 string s abcdefg 2 获取字符串长度 s Length 3 比较字符串是否一样 s abcd 4 字符串连接 s http s 5 使用类似索引器的语法来取得字符串中的某个字符
  • Android常见漏洞

    Android常见漏洞 漏洞名称 Log敏感信息泄露 漏洞描述 程序运行期间打印了用户的敏感信息 造成泄露 修改建议 建议禁止隐私信息的log 漏洞名称 web https校验错误忽略漏洞 漏洞描述 漏洞可导致中间人攻击 修改建议 建议不要
  • JAVA基础练习题

    1 生成两个1 10的随机数 分别作为两个数组的长度 2 向第一个数组中以循环键盘录入的方式添加元素 3 生成1 100之间的随机数 为第二个数组的每个元素赋值 4 将两个数组合并 为一个新的数组 5 去掉新数组的最大值和最小值 求平均值
  • vue之--使用TypeScript

    搭配 TypeScript 使用 Vue 像 TypeScript 这样的类型系统可以在编译时通过静态分析检测出很多常见错误 这减少了生产环境中的运行时错误 也让我们在重构大型项目的时候更有信心 通过 IDE 中基于类型的自动补全 Type
  • C++ 好用的日志库--spdlog

    背景 spdlog 是一个快速 异步的 header only 的 C 日志库 它提供了简单易用的 API 并具有高性能和可扩展性 下载和使用 下载 spdlog 库下载地址 github 链接 hello world 在使用时只需要 in
  • 设计模式:模板方法模式

    定义一个操作中算法的框架 而将一些步骤延迟到子类中 使得子类可以不改变算法的结构即可重定义该算法中的某些特定步骤 类图如下 事实上 模版方法是编程中一个经常用到的模式 先来看一个例子 某日 程序员A拿到一个任务 给定一个整数数组 把数组中的
  • 常用的函数式接口

    1 Supplier接口 仅包含一个无参的方法 T get 用来获取一个泛型参数指定类型的数据 2 Consumer接口 包含抽象方法 void accept T t 正好与Supplier相反 它不是生产一个数据 而是 消费一个数据 其数
  • web安全--环境搭建

    1 搭建环境 下载owasp靶机测试环境 login root password owaspbwa 下载kali linux攻击机也可以使用windows作为攻击机 我的是2020 login kaili password kali 其他版
  • typescript学习(二)——函数

    一 函数的定义 1 函数声明法 规定返回值必须为string类型 function run string return im string 2 匿名函数法 let run function string return im string 没
  • uniapp实现下拉加载更多 u-loadmore

    区别于官网教程 这里总结更为白话 要实现的需求是 比如起初有十条数据 下拉至底部 追加某数量的数据 比如我的项目中 我把它放在了数据展示的底部 上代码
  • 微信支付踩坑记-签名错误,请检查后再试

    注册商户号 使用微信支付功能 直接无脑设置v3密钥 然后配合wxjava这个包进行开发 结果试了很多遍 都报错 最后才发现 我一直使用的v2验证方式 之前都没有设置过v2密钥 最后把v2密钥设置成和v3一样 重新试了一下 解决问题 最后附上
  • Python3学习(六):函数

    Python3 函数 函数是组织好的 可重复使用的 用来实现单一 或相关联功能的代码段 函数能提高应用的模块性 和代码的重复利用率 你已经知道Python提供了许多内建函数 比如print 但你也可以自己创建函数 这被叫做用户自定义函数 可
  • 广度优先算法

    deque 即双端队列 是一种具有队列和栈的性质的数据结构 双端队列中的元素可以从两端弹出 其限定插入和删除操作在表的两端进行 最短路径问题的算法被称为广度优先搜索 广度优先搜索是一种用于图的查找算法 第一类问题 从节点A出发 有前往节点B
  • Pandas 写入 Excel 的几种情形与方式,覆盖,新增,追加,对齐

    Pandas 写入 Excel 的几种情形与方式 覆盖 新增 追加 对齐 以下代码默认已经导入 np pd import numpy as np import pandas as pd 执行下面示例之前 最好先删除 test xlsx 文件
  • 怎样去掉PATH中重复的项目

    在zsh shell 中输入 typeset U path 运行即可
  • IDEA中TDengine数据库连接

    过程描述 首先配置数据库连接信息 下载驱动jar包 配置驱动 Maven Repository com taosdata jdbc taos jdbcdriver mvnrepository com 下载 TDengine Client 安
  • Java web小项目_个人主页(1)—— 云环境搭建与项目部署

    摘自 Java web小项目 个人主页 1 云环境搭建与项目部署 作者 丶PURSUING 发布时间 2021 03 26 23 59 39 网址 https blog csdn net weixin 44742824 article de
  • Git & Github 的使用(配合VSCode)

    1 git 和 vscode 环境管理 git下载位置 根据自己的电脑配置进行下载即可 Git Downloads 比如我的是linux ubuntu 那么我就选择Linux 并在命令行下敲下来这个 下载完git后 可以在终端上输入命令 g
  • Junit

    TestCase之间一定要保持完全的独立性 不允许出现任何的依赖关系 不能依赖与测试方法的执行顺序 DRY Don t Repeat Yourself Junit一旦执行到了Assert fail 方法测试便失败
  • 五分钟搞懂 React Hooks 工作原理

    前言 Hooks 是 React 中比较火的一个概念 用过的小伙伴都说好 但是 在使用 Hooks 的时候 我们可能会有很多疑惑 1 为什么 useEffect 第二个参数是空数组 就相当于 ComponentDidMount 只会执行一次