【React Hooks - useState状态批量更新原理】

news2024/9/22 13:36:32

概述

所谓批量处理就是当在同时更新多个状态下,能够统一批量处理更新,避免了重复渲染。在React17及之前版本,React只会在合成事件以及生命周期内部进行批量处理,在setTimeout、Promise、Fetch等异步请求中,则不会自动批量处理,需要使用unstable_batchedUpdatesAPI手动处理。而在React18对其进行了优化,不管什么条件下,默认都会批量处理。本文主要就是从demo实例结合bugger源码的方式来解释在React17和18中对于状态批量更新的逻辑介绍。

React17

从概述可知,React17版本,默认只会在合成事件、生命周期内批量处理,在异步请求中需要手动处理,先看下面demo代码:

import React, { Fragment, useState } from 'react';

export default function Component() {
  const [a, setA] = useState(1);
  console.log('a', a);

  // 异步请求,不会自动批量,会渲染多次,该示例中会render4次
  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      setA((a) => a + 1);
      setA((a) => a + 1);
      setA((a) => a + 1);
      setA((a) => a + 1);
    });
  }
  // 绑定点击事件,会自动批量,只会render一次
  function handleClickWithoutPromise() {
    setA((a) => a + 1);
    setA((a) => a + 1);
    setA((a) => a + 1);
    setA((a) => a + 1);
  }

  return (
    <Fragment>
      <button onClick={handleClickWithPromise}>{a} 异步执行</button>
      <button onClick={handleClickWithoutPromise}>{a} 同步执行</button>
    </Fragment>
  );
}

先解释一下上面说的合成事件:React 中的合成事件是 React 自己实现的一套跨浏览器兼容的事件处理机制。它将浏览器原生事件封装为统一的 API,以确保在不同浏览器中的行为一致。通过事件委托和事件池化,React 可以更高效地管理事件监听器,并减少内存开销。合成事件使得开发者能够以一致的方式处理各种用户交互事件,无需关心浏览器之间的差异。即如下图所示:
在这里插入图片描述
上面的demo在浏览器中(Chrome为例),点击同步按钮会打印5,点击异步按钮则会打印2,3,4,5render4次。

因为React会自动合并,所以只能通过setA((a) => a + 1)或者setA(2)这种具体值的方式才会符合上述,由于React会自动尝试合并操作,如果书写为setA(a + 1)则只会打印2次(由于 React 在异步上下文中处理状态更新时的行为,React 17 可能会导致组件重新渲染两次)

接下来我们在浏览器开启debugger来了解其内部逻辑。

同步执行

同步执行下,会自动批量处理,只会render一次。
在bugger下我们点击同步按钮能看到整个函数执行的调用栈,其中主要看标记的几个函数:
在这里插入图片描述
其中顶层函数就是我们点击同步按钮执行的回调,然后继续单步调整会发现其进入了React的dispatchAction来创建一个状态更新任务,然后会调用scheduleUpdateOnFiber进入Scheduler调度器中等待执行,这个阶段本文不再介绍,有兴趣的可以查看这篇文章:【React Hooks原理 - useState】

直到最后一个状态更新执行完成,会根据当前调用栈往上回调,然后来到标记的第二个函数batchedEventUpdates$1

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

当我们在页面点击同步按钮时,就会触发React的合成事件,进而进入上面的函数中,在其中主要做以下事:

  • 保存当前上下文,并更新当前上下文为EventContext,默认是NoContext
  • 执行传入的回调函数
  • 回退当前上下文为自身的上下文,并判断是否执行更新

由于是同步执行,所以当状态更新回调执行完后,会进入上面的代码中的finally,此时所有的状态更新都保存在更新队列中的,然后执行flushSyncCallbackQueue回调,进行批量更新,所以只会render一次。

异步执行

在setTimeout、Promise、Fetch等异步回调中,不会自动批量处理,需要手动使用unstable_batchedUpdates。如上述所说,在异步条件下,上面的demo会render4次。
在这里插入图片描述
从图中能看出,点击按钮时也会经过batchedEventUpdates函数的封装,并在其设置上下文进去批量更新(同步逻辑),但是在异步情况下,异步回调会在当前任务执行完成之后在执行,执行时已经脱离的设置的批量上下文,所以当进入finally中批量更新时,此时更新队列并没有当前新的更新任务,等到更新任务执行时,此时上下文已经不再是批量上下文,所以会依次执行状态更新而导致重复render。

unstable_batchedUpdates

使用该API可以强制将其中的回调同步执行,可以用于在异步请求中批量处理。其本质就是batchedUpdates$1函数,所以当在异步请求中将状态更新放在其内部,会批量处理。
在这里插入图片描述
通过bugger也能发现,其实际还是执行的batchedUpdates$1函数,逻辑和同步一致,通过设置上下文然后调用flushSyncCallbackQueue()批量处理更新任务,区别就是由于其仍然处于异步回调用,所以执行时机仍然会延迟,等待同步代码执行完成之后执行。
在这里插入图片描述

总结

在点击按钮触发状态更新时,实际触发的是经过batchedUpdates$1处理的合成事件。同步代码中在状态更新时将更新任务添加到队列中(此时上下文已经更新为批量上下文),最后在finally中执行flushSyncCallbackQueue批量更新状态。而在异步回调中会脱离批量上下文,通过使用unstable_batchedUpdates包裹,收到执行batchedUpdates$1函数,在执行时重新设置批量上下文,并调用flushSyncCallbackQueue批量更新,本质还是通过batchedUpdates$1函数执行批量,无非一个是自动一个是手动的区别。

React18

在React18之后,主要新增了并发特性和对批量更新进行了优化,不管在异步还是同步回调中都默认进行批量处理。下面同样使用上面的demo代码,在Chrome下不管点击同步还是异步按钮都只会render一次。下面在React18环境下进行bugger流程介绍。

同步执行

在浏览器调试我们知道,在React17中当对状态进行更新的时会通过dispatchAction调用scheduleUpdateOnFiber等待调度更新,而在18中对其进行了优化,不会直接调度更新,而是在dispatchSetState中通过enqueueConcurrentHookUpdate将状态更新添加到等待执行的队列中,待执行完成之后再统一批量更新。
在这里插入图片描述
同React17,在18中状态更新回调执行完成之后,会回到batchedUpdates$1函数,此时所有的更新任务都以链表的方式保存在队列中。

function batchedUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext; // If there were legacy sync updates, flush them at the end of the outer
    // most batchedUpdates-like method.

    if (executionContext === NoContext && // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
    !( ReactCurrentActQueue$1.isBatchingLegacy)) {
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
  }
}

如上面所说,该函数主要就是设置批量上下文,执行传入的更新回调,然后在finally中通过flushSyncCallbacksOnlyInLegacyMode函数然后执行flushSyncCallbacks同步更新状态。

function flushSyncCallbacksOnlyInLegacyMode() {
  // Only flushes the queue if there's a legacy sync callback scheduled.
  // TODO: There's only a single type of callback: performSyncOnWorkOnRoot. So
  // it might make more sense for the queue to be a list of roots instead of a
  // list of generic callbacks. Then we can have two: one for legacy roots, one
  // for concurrent roots. And this method would only flush the legacy ones.
  if (includesLegacySyncCallbacks) {
    flushSyncCallbacks();
  }
}

异步执行

React18对其优化之后,在异步请求中默认也会自动批量处理。和同步时一样也会通过enqueueConcurrentHookUpdate将更新任务添加到更新队列中,并不会直接调度更新。当执行到最后一个状态更新的setState时,会进入ensureRootIsScheduled的这块逻辑进入微任务的处理(其他setState会在上面就return,不会进入该逻辑):

scheduleMicrotask(function () {
          // In Safari, appending an iframe forces microtasks to run.
          // https://github.com/facebook/react/issues/22459
          // We don't support running callbacks in the middle of render
          // or commit so we need to check against that.
          if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
            // Note that this would still prematurely flush the callbacks
            // if this happens outside render or commit phase (e.g. in an event).
            flushSyncCallbacks();
          }
        });

在其中会执行flushSyncCallbacks函数统一处理更新队列中的任务,最后只渲染一次。所以在React18中不管是同步还是异步,都是先将更新任务保存在待执行队列中,最后都是通过flushSyncCallbacks来批量处理状态更新的。

总结

同步更新: 状态更新任务入队并在 flushSyncCallbacks 中被批量处理。
异步更新: 状态更新任务同样入队,但在异步任务完成后,通过微任务调度机制调用 flushSyncCallbacks 来批量处理这些任务。

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

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

相关文章

【GH】【EXCEL】P1: Write DATA SET from GH into EXCEL

文章目录 WriteFast WriteGH data material :GH process and components instructionFast Write DataFast Write Data & Clear DataFast Write to Cell EXCEL written results Write by ColumnGH data material :Compile ColumnGH process and components instructionWrite…

三、Kafka副本

2、创建2个分区两个副本 /usr/local/kafka/bin# ./kafka-topics.sh --bootstrap-server 192.168.58.130:9092 --create --topic atguigu2 --partitions 2 --replication-factor 23、查看topic详细信息 /usr/local/kafka/bin# ./kafka-topics.sh --bootstrap-server 192.168.5…

如何理解CAPL—Test编程中的测试对象

前言&#xff1a;CAPL—Test编程中的对象&#xff0c;是一个比较复杂的概念&#xff0c;对象的作用是作为Test特定函数的参数。来执行特定的功能&#xff08;这是比较复杂的一个概念&#xff0c;下文会慢慢讲解&#xff09;。 注意&#xff1a;因为翻译的问题&#xff0c;有些…

arm:ADC模数转换器

比较器 AD&#xff1a; 精度&#xff1a;10位 转换速率&#xff1a;500 KSPS 量程&#xff1a;0~3.3v void adc_init(void) {ADCCON (1 << 14) | (49 << 6) | (1 << 1); }unsigned short adc_read(void) {unsigned short value ADCDAT0;while(~(ADCCON &am…

华为M60首次降价,消费回暖能延续?

导语 8月15日&#xff0c;华为Mate 60系列首次官宣降价&#xff01;能否带动消费电子进一步回暖&#xff1f; 在当前全球经济形势复杂多变的背景下&#xff0c;各行各业都在寻求新的增长点和突破口。 消费电子市场作为科技与日常生活紧密结合的重要领域&#xff0c;其发展态势一…

基于HarmonyOS的宠物收养系统的设计与实现(一)

基于HarmonyOS的宠物收养系统的设计与实现&#xff08;一&#xff09; 本系统是简易的宠物收养系统&#xff0c;为了更加熟练地掌握HarmonyOS相关技术的使用。 项目创建 创建一个空项目取名为PetApp 首页实现&#xff08;组件导航使用&#xff09; 官方文档&#xff1a;组…

微服务中的Sidecar模式

微服务中的Sidecar模式 什么是sidecarsidecar如何工作Sidecar 代理服务注册发现Sidecar 代理异构服务发起服务调用异构服务如何被调用 常见应用以MOSN流量接管为例使用 sidecar 模式的优势sidecar和面向切片编程AOP的关系参考 什么是sidecar sidecar是服务网络架构的产物。 S…

【网络】 arp 命令 得到网段内所有物理设备ip

我的笔记本和 NVIDIA Jetson Orin 都位于同一个 192.168.1.x 的网段内&#xff0c;我想远程访问 Orin&#xff0c;但我不知道orin的ip 方法 1: 使用 arp 命令 打开命令提示符&#xff1a; 按下 Win R 键&#xff0c;打开“运行”对话框。输入 cmd 并按 Enter 键打开命令提示符…

JAVA类加载过程/类装载的执行过程/java类加载机制/JVM加载Class文件的原理机制?

JAVA类加载过程/类装载的执行过程/java类加载机制/JVM加载Class文件的原理机制&#xff1f; 类加载的过程主要分为三个部分&#xff1a;&#xff08;加链初&#xff0c;验准解&#xff09; 加载链接初始化 而链接又可以细分为三个小部分&#xff1a; 验证准备解析 骚戴理解…

散点图适用于什么数据 thinkcell散点图设置不同颜色

在数据可视化的众多工具和技巧中&#xff0c;散点图是一种极为有效的方式&#xff0c;能够揭示变量之间的关系&#xff0c;尤其是在探索数据集的相关性、分布趋势、集群现象时。而在众多助力于制作高质量散点图的工具中&#xff0c;think-cell插件以其高效的操作和丰富的功能&a…

重定向与追加

1、>和>> > 【重定向】 如果文件不存在&#xff0c;则创建&#xff0c;并将内容输入到文件&#xff1b; 如果文件存在&#xff0c;则先清空文件&#xff0c;然后将内容输入到文件&#xff1b;>> 【追加】 如果文件不存在&#xff0c;则创建&…

自监督学习self-supervised learning

Tags: #tutorial #machine-leanring #self-supervised 目录&#xff1a; The Importance of Self-Supervised Learning Popular Learning Methods Introduction to Self-Supervised Learning 1. The Inportance of Self-Supervised Learning 监督学习(supervised learnin…

【面试】tomcat类加载机制

目录 1. 说明2. 主要类加载器2.1 Bootstrap类加载器2.2 Common类加载器2.3 Catalina类加载器2.4 Shared类加载器2.5 Web应用类加载器2.5 JSP类加载器 3. 图示4. 特点5. 加载顺序6. 面试题 1. 说明 1.tomcat的类加载机制是违反了双亲委派原则的&#xff0c;对于一些未加载的非基…

初识指针3の学习笔记

目录 1>>前言 2>>冒泡排序 3>>二级指针 4>>指针数组 5>>指针数组模拟二维数组 6>>结语 1>>前言 今天我会继续分享一些我做的笔记&#xff0c;以及我对指针的理解&#xff0c; 后续会持续分享指针几天&#xff0c;毕竟指针的内…

实验室安全分级分类管理系统在高校中的具体应用

盛元广通高校实验室安全分级分类管理系统的构建&#xff0c;旨在通过科学合理的管理手段&#xff0c;提高实验室的安全水平&#xff0c;保障师生的人身安全&#xff0c;防止实验事故的发生。这一系统通常包括实验室安全等级评估、分类管理、风险控制、安全教育与培训、应急响应…

[Qt][绘图][上]详细讲解

目录 0.为什么&#xff1f;1.绘图API核心类2.设置画笔3.设置画刷4.设置画家1.移动画家位置2.保存/加载画家的状态 0.为什么&#xff1f; 虽然Qt已经内置了很多的控件&#xff0c;但是不能保证现有控件就可以应对所有场景&#xff0c;很多时候&#xff0c;需要更强的"定制…

第N8周:使用Word2vec实现文本分类

本文为365天深度学习训练营 中的学习记录博客原作者&#xff1a;K同学啊 一、数据预处理 任务说明: 本次将加入Word2vec使用PyTorch实现中文文本分类&#xff0c;Word2Vec 则是其中的一种词嵌入方法&#xff0c;是一种用于生成词向量的浅层神经网络模型&#xff0c;由Tomas M…

spring boot学习第二十篇:使用minio上传下载文件获取文件路径

先安装好minio&#xff0c;参考&#xff1a;window10安装minio-CSDN博客 1、pom.xml文件添加依赖&#xff1a; <!-- 操作minio的java客户端--><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.…

C语言:ifswitch分支语句

目录 前言 一、if语句 1.1 if 1.2 else 1.3 嵌套if 1.4 悬空else问题 二、switch语句 2.1 if语句和switch语句的对比 2.2 switch语句中的default 前言 C语⾔是结构化的程序设计语言&#xff0c;这里的结构指的是顺序结构、选择结构、循环结构&#xff0c;C语言是能够实…

大模型之战-操作数据表-coze

工作流直接操作数据库啦【何时可以直接访问自己的数据库呢】 1&#xff0c;第一步创建一个bot智能体 1.1&#xff0c;bot中创建数据库表&#xff1a; 1.2&#xff0c;智能体可以通过对话&#xff0c;操作表&#xff1b;【增加&#xff0c;筛选查询等】 1.2.1&#xff0c;增加…