React -- useState状态更新异步特性——导致获取值为旧值的问题

useState状态异步更新

  • 问题
  • 导致的原因
  • 解决办法
  • 进一步分析
  • 后续遇到的新问题

问题

  const [isSelecting, setIsSelecting] = useState(false);useEffect(() => {const handleKeyDown = (event) => {if (event.key === 'Escape') {if(isSelectingRef){//.......setIsSelecting(!isSelecting);console.log("执行了么")}}};window.addEventListener('keydown', handleKeyDown);return () => {window.removeEventListener('keydown', handleKeyDown);};}, [editor]); ........
<Buttonsize="xs"className={`bg-[#21242a] text-xs mt-1 mb-1 ${isSelecting?'w-20':"w-full"}`}onClick={() => {editor.isSelectingDyObTarckPoint = !editor.isSelectingDyObTarckPoint;document.body.style.cursor = "crosshair";if(editor.dyObstacleTrackPoint.length===0){//...........}if (isSelecting) {console.log("执行了么?")//......}setIsSelecting(!isSelecting);}}>{isSelecting ? "Done" : "Edit track"}</Button>

当时的场景,主要是为了设置一个esc快捷键,esc快捷键的逻辑功能和按钮为“Done”的时候点击效果是一样的。(主要为了方便,直接键盘操作);
但是发现isSelecting初始值为false(按钮渲染为Edit track),在第一次点击按钮时,isSelecting设置新值为!isSelecting即为true(按钮渲染为Done)。
此时按下esc,打印出来的isSelecting为false,条件判断内的逻辑没有被执行。

导致的原因

在这里插入图片描述
在这里插入图片描述

如果点击按钮后 isSelecting 应该变成 true 但是打印出来却是 false,这是因为在日志输出时遇到了 React 的状态更新异步特性。
在 React 中,当你调用 setIsSelecting 来更新状态时这个操作是异步的。这意味着状态不会立即更新,而是会在下次组件重新渲染时更新。因此,如果你在调用 setIsSelecting 后立即打印 isSelecting 的值,它可能还没有更新。

例如,以下代码中的 console.log 将输出状态更新之前的值:

setIsSelecting(true); 
// 这里更新了状态,但这个操作是异步的
console.log(isSelecting); 
// 这里很可能还是旧的状态值,因为状态更新是异步的

要检查状态更新之后的值,你可以使用 useEffect 钩子来“监听”isSelecting 状态的变化:

useEffect(() => {console.log(isSelecting); // 当 isSelecting 更新后,这里会输出新值
}, [isSelecting]); 
// 依赖数组确保只有当 isSelecting 变化时才执行这个 effect

解决办法

使用useRef记录值。
要确保在 handleKeyDown 函数中捕获到最新的 isSelecting 状态值,可以使用 useRef 钩子来确保引用保持最新。因为 useRef 创建的对象会在整个组件的生命周期内保持不变,我们可以利用这一点来存储最新的状态。

import React, { useState, useEffect, useRef } from 'react';
// ...省略其他imports...const SimpleProperties = ({ apaObject, enabledItems ,editor}) => {const [isSelecting, setIsSelecting] = useState(false);// ...省略其他状态和逻辑...// 使用 useRef 来跟踪当前的 isSelecting 状态。const isSelectingRef = useRef(isSelecting);// 每当 isSelecting 改变时,更新 ref 的 current 值useEffect(() => {isSelectingRef.current = isSelecting;}, [isSelecting]);// 键盘事件处理器使用 ref 来获取最新的 isSelecting 值const handleKeyDown = (event) => {if (event.key === 'Escape') {// 通过 isSelectingRef.current 获取最新的状态值if (isSelectingRef.current) {console.log("就是不执行")// ...你原有的逻辑...// 更新状态setIsSelecting(!isSelectingRef.current);}}};// 设置键盘事件监听useEffect(() => {// 添加keydown事件监听器window.addEventListener('keydown', handleKeyDown);// 清除事件监听器,当组件卸载时执行return () => {window.removeEventListener('keydown', handleKeyDown);};}, []); // 这里的依赖数组为空,表示 effect 只在挂载和卸载时运行// ...省略其他部分...
};

在这个修改版的代码中,isSelectingRef 是一个 ref 对象,它的 current 属性始终包含最新的 isSelecting 状态值。handleKeyDown 函数通过访问 isSelectingRef.current 来获取最新状态值,而不是直接从闭包中获取。这样做的好处是无论何时调用 handleKeyDown,它都能获取到最新的状态值。

请注意,设置键盘事件监听的 useEffect 中的依赖数组被设置为空([]),意味着这个 effect 只在组件挂载时添加事件监听器,并且在组件卸载时移除。由于我们不需要响应任何特定的属性或状态的变化来重新绑定事件监听器,这样做是安全的。如果你希望对某些属性或状态做出响应,则需要相应地更新依赖数组。

进一步分析

在 React 的 useEffect 钩子中使用事件处理器时,如果事件处理器引用了组件的状态或属性,并且这些状态或属性在函数定义时的值被固定下来,那么我们就说这个事件处理器是一个闭包,并且它“捕获”了定义它时的环境。

例如,在下面的代码中:

useEffect(() => {const handleKeyDown = (event) => {if (event.key === 'Escape') {// 这里直接访问了 isSelecting,这个值是 handleKeyDown 函数定义时的值console.log(isSelecting);}};window.addEventListener('keydown', handleKeyDown);return () => {window.removeEventListener('keydown', handleKeyDown);};
}, []); // 依赖数组为空,effect 只在挂载和卸载时运行

handleKeyDown 是在 useEffect 的回调函数内部定义的。当这个 useEffect 执行时,它会创建 handleKeyDown 函数,并把当前的 isSelecting 状态“捕获”到该函数内部。这意味着无论 isSelecting 状态以后如何变化,handleKeyDown 内部看到的总会是创建它那一刻的 isSelecting 值。因为 useEffect 的依赖数组为空,所以它只在组件挂载时运行一次,而不会在 isSelecting 更新时重新运行。

要解决这个问题,通常有两种方法:

  1. 将状态更新逻辑放入 useEffect 并将状态添加到依赖数组中。但这会导致注册和注销监听器的频繁操作
  2. 使用 useRef 来存储状态的最新值。因为 ref 对象是可变的,并且不会触发组件的额外渲染,所以事件处理器可以通过查看 ref 的 current 属性来访问最新值,而不依赖于闭包中捕获的旧值。

使用 useRef 的方案允许你在不重新绑定事件监听器的情况下,始终访问到最新的状态值。这就是为什么在之前的答案中我建议使用 useRef 的原因。


后续遇到的新问题

在handledown中使用isSelecting方法没有反应,这意味着 setIsSelecting 可能没有被正确调用,或者 isSelecting 状态的更新因为某些原因没能触发重新渲染。以下是一些解决这个问题的步骤:

  1. 确认事件监听器被触发:
    首先确认 handleKeyDown 函数确实在按下 Esc 键时被触发。可以通过在该函数内部加入一个 console.log 来确认。
const handleKeyDown = (event) => {if (event.key === 'Escape') {console.log('Escape key pressed');setIsSelecting(current => !current); // 使用函数式更新// 其他逻辑...}
};
  1. 检查 useEffect 的依赖数组:
    请确保你在 useEffect 中注册和注销事件监听器的代码不受其他状态或属性的影响。如果你把 handleKeyDown 定义在 useEffect 内部,并且该 useEffect 没有依赖项,则不需要担心闭包问题。

  2. 使用函数式 setState:
    当你想要基于当前状态来更新状态时,使用函数式 setState 可以避免因闭包导致的过时状态值引用问题。(使用该方法解决了)

setIsSelecting(current => !current);

分析
闭包捕获的是函数定义时所在作用域内的变量,并且这些捕获的变量不会随着外部作用域中同名变量的变化而更新。这是因为函数通过闭包保持对其创建时环境的引用,就像它们“记住”了那些变量及其当时的值。

下面是一个简单的闭包例子来说明这个概念:

function createFunction() {let value = 1;return function() {console.log(value);};
}let myFunction = createFunction();
value = 2;
myFunction(); // 输出 1, 而不是 2

在上述示例中,myFunction 是在 createFunction 中创建的,它“记住”了变量 value 当时的值(1),尽管后来该变量的值已经改变。当调用 myFunction() 时,它仍然输出 1。

在 React 组件的上下文中,每次组件重新渲染时都会生成新的函数实例和变量。但如果你使用 useEffect 钩子并且依赖数组为空([]),或者将依赖项排除在 useEffect 外,事件处理器将只会在第一次渲染时被创建一次,它会捕获并“记住”当时的状态和属性值。

例如:

useEffect(() => {const handleClick = () => {console.log(value); // 这里的 value 是 handleClick 被创建时的值};document.addEventListener('click', handleClick);return () => {document.removeEventListener('click', handleClick);};
}, []); // 空依赖数组使得 useEffect 只在组件挂载时运行

即使组件重新渲染并且 value 的值发生变化,handleClick 定义时捕获的 value 仍然是旧值,并且由于 useEffect 的依赖数组为空,handleClick 并不会重新定义。因此,无论触发多少次点击事件,handleClick 总是输出最初捕获的 value 值。

正因为如此,要确保事件处理器总能够获取到最新的状态和属性,你需要使用函数式更新(如 setState(current => current + 1))或确保相关变量和状态作为依赖被包含在 useEffect 的依赖数组中,从而在它们更新时重新创建事件处理器。

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

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

相关文章

封装了一个仿照抖音效果的iOS评论弹窗

需求背景 开发一个类似抖音评论弹窗交互效果的弹窗&#xff0c;支持滑动消失&#xff0c; 滑动查看评论 效果如下图 思路 创建一个视图&#xff0c;该视图上面放置一个tableView, 该视图上添加一个滑动手势&#xff0c;同时设置代理&#xff0c;实现代理方法 (BOOL)gestur…

如何理解JavaScript代理对象(JavaScript Proxy)

JavaScript的Proxy对象是一种强大且灵活的特性&#xff0c;它允许你拦截并自定义对对象执行的操作。自ECMAScript 6&#xff08;ES6&#xff09;引入以来&#xff0c;Proxy对象为控制对象的基本操作行为提供了一种机制&#xff0c;使高级用例和改进的安全性成为可能。 代理对象…

HNTs-g-PEG-CDs-Biotin NPs;碳量子点修饰接枝生物素化的羟基磷灰石纳米管

HNTs-g-PEG-CDs-Biotin NPs&#xff0c;即碳量子点修饰接枝生物素化的羟基磷灰石纳米管&#xff0c;是一种结合了多种先进材料特性的纳米复合材料。以下是对该材料的详细分析&#xff1a; 一、组成成分及特性 羟基磷灰石纳米管&#xff08;HNTs&#xff09;&#xff1a; 羟基磷…

elasticSearch的索引库文档的增删改查

我们都知道&#xff0c;elasticsearch在进行搜索引擎的工作时&#xff0c;是会先把数据库中的信息存储一份到elasticsearch中&#xff0c;再去分词查询等之后的工作的。 elasticsearch中的文档数据会被序列化为json格式后存储在elasticsearch中。elasticsearch会对存储的数据进…

重庆交通大学数学与统计学院携手泰迪智能科技共建的“智能工作室”

2024年7月4日&#xff0c;重庆交通大学数学与统计学院与广东泰迪智能科技股份有限公司携手共建的“智能工作室”授牌仪式在南岸校区阳光会议室举行。此举标志着数统学院与广东泰迪公司校企合作新篇章的开启&#xff0c;也预示着学院在智能科技教育领域的深入探索和实践。 广东…

代发考生战报:南京考场华为售前HCSP H19-411考试通过

代发考生战报&#xff1a;南京考场华为售前HCSP H19-411考试通过&#xff0c;客服给的题库非常稳定&#xff0c;考试遇到2个新题&#xff0c;剩下全是题库里的原题&#xff0c;想考的放心考吧&#xff0c;考场服务挺好&#xff0c;管理员带着做签名和一些考试说明介绍清楚&…

科研绘图系列:R语言分组柱状图一(Grouped Bar Chart)

介绍 分组柱状图(Grouped Bar Chart)是一种数据可视化图表,用于比较不同类别(分组)内各子类别(子组)的数值。在分组柱状图中,每个分组有一组并列的柱子,每个柱子代表一个子组的数值,不同的分组用不同的列来表示。 特点: 并列柱子:每个分组内的柱子是并列的,便于…

51 单片机[7]:计时器

一、定时器 1. 定时器介绍 51单片机的定时器属于单片机的内部资源&#xff0c;其电路的连接和运转均在单片机内部完成。 定时器作用&#xff1a; &#xff08;1&#xff09;用于计时系统&#xff0c;可实现软件计时&#xff0c;或者使程序每隔一固定时间完成一项操作 &#…

运算符和表达式

运算符 运算&#xff1a;对数据进行加工和处理。 运算符&#xff1a;表示各种运算的符号。 操作数&#xff1a;参与运算的数据。 根据操作数的个数&#xff0c;可以将运算符分为单目、双目和多目运算符。单目运算符只对1个操作数运算&#xff0c;双目运算符对2个操作数运算…

k8s中port,targetPort,nodePort,containerPort的区别

一、说明 在 Kubernetes 中&#xff0c;port、targetPort、nodePort 和 containerPort 是用于定义服务&#xff08;Service&#xff09;和容器之间网络通信的不同参数。 它们各自的作用和含义如下&#xff1a; 1. port 定义&#xff1a;这是服务对外暴露的端口号。作用&#x…

linux指令练习

二、touch、vi练习&#xff1a; 1、在root家目录下创建目录A1和B1 2、进入B1下同时创建三个文件m1, m2 , n1&#xff0c;单独创建目录N1 3、进入到A1目录中分别创建一个文件t1,k2&#xff0c;同时创建目录F1&#xff0c;F2 4、删除B1下的所有1结尾的文件或者目录 5、删除A1目录…

Python基础知识——(001)

文章目录 P4——3. 程序设计语言的分类 1. 程序设计语言 2. 编译与解释 P5——4. Python语言的简介与开发工具 1. Python语言的简介 2. Python语言的发展 3. Python语言的特点 4. Python的应用领域 5. Python的开发工具 P6——5. IPO编程方式 IPO程序编写方法 P7——6. print函…

案例精选 | 聚铭综合日志分析系统为江苏省电子口岸构建高效安全的贸易生态

江苏省电子口岸有限公司&#xff0c;成立于2009年&#xff0c;由江苏省贸促会携手南京海关、江苏检验检疫局及江苏海事局等部门共同出资组建。公司承载着推动江苏乃至长三角地区国际贸易便利化的重大使命&#xff0c;致力于打造一个集先进性、创新性、高效性于一体的电子口岸综…

STM32初识HAL库(下载和使用)

初识HAL库&#xff08;了解&#xff09; ST 为了方便用户开发 STM32芯片开发提供了三种库&#xff1a; 标准外设库 (Standard Peripheral Libraries)HAL库(硬件抽象层)&#xff1a;Hardware Abstraction LayerLL库&#xff1a;Low Layer 一、获取STM32Cube固件包 方式一&…

jQuery 笔记

一、什么是jQuery 框架&#xff1a;半成品软件 Jquery就是封装好的js 本质上还是js jQuery是一个快速、简洁的JavaScript**框架**&#xff0c;是继Prototype之后又一个优秀的**JavaScript代码库**&#xff08;*或JavaScript框架*&#xff09;。 JQuery:封装好的代码库。有一…

【Proteus】按键的实现『⒉种』

&#x1f6a9; WRITE IN FRONT &#x1f6a9; &#x1f50e; 介绍&#xff1a;"謓泽"正在路上朝着"攻城狮"方向"前进四" &#x1f50e;&#x1f3c5; 荣誉&#xff1a;2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2222年获评…

Qt 进程间通信(一)——QSharedMemory共享内存

QSharedMemory共享内存 序言环境理论—逻辑理解实战—代码读取示例写入示例 序言 讲讲Qt的共享内存吧&#xff0c;巩固下 环境 msvc2022 Qt5.15 参考文档&#xff1a;https://doc.qt.io/qt-5/qsharedmemory.html 理论—逻辑理解 看下面前&#xff0c;你需要将共享内存看成…

JS数据类型检测的方式有哪些 (常用)

typeof 其中数组、对象、null都会被判断为object&#xff0c;其他判断都正确typeof返回的类型都是字符串形式 instanceof instanceof &#xff1a;用于检测一个实例是否属于某个类&#xff0c;通过验证当前类的原型 prototype 是否出现在实例的原型链 __proto__ 上。它不能检测…

如何在Excel中对一个或多个条件求和?

在Excel中&#xff0c;基于一个或多个条件的求和值是我们大多数人的常见任务&#xff0c;SUMIF函数可以帮助我们根据一个条件快速求和&#xff0c;而SUMIFS函数可以帮助我们对多个条件求和。 本文&#xff0c;我将描述如何在Excel中对一个或多个条件求和&#xff1f; 在Excel中…

DataExcelServer局域网文件共享服务器增加两个函数

1、PFSUM合并指定路径下单元格ID的值 PFSUM("/103采购/8月采购名细","amount") 第一个参数为路径&#xff0c;第二个参数为单元格的ID 2、PFQuery 查询路径下 单元格ID值的列表 PFQuery("/103采购/8月采购名细","amount") 查询/103采…