当前位置: 首页 > news >正文

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 的依赖数组中,从而在它们更新时重新创建事件处理器。

相关文章:

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

useState状态异步更新 问题导致的原因解决办法进一步分析后续遇到的新问题 问题 const [isSelecting, setIsSelecting] useState(false);useEffect(() > {const handleKeyDown (event) > {if (event.key Escape) {if(isSelectingRef){//.......setIsSelecting(!isSele…...

哪款开放式耳机是2024年最值得购买的?五大品质好物揭秘

相比于入耳式耳机压耳、堵耳&#xff0c;佩戴不稳固等缺陷&#xff0c;开放式耳机的佩戴舒适性和安全性都更胜一筹&#xff0c;这几年成为了越来越多年轻人的“音乐搭子”。面对市面上各式各样的开放式耳机&#xff0c;相信大家在挑选上就得下大把功夫&#xff0c;选择上也有困…...

深圳天童美语:小暑习俗知多少

小暑已至&#xff0c;炎炎夏日正当时。在这个充满生机的节气里&#xff0c;除了我们熟悉的吃冰、游泳等消暑方式&#xff0c;还有许多有趣且富含文化内涵的小暑习俗。今天&#xff0c;深圳天童美语就带你一起解锁这些习俗&#xff0c;感受那份独特的夏日风情&#xff01;    …...

递归参数中递增运算符的使用

backtrack(k,n,sum,i1); backtrack(k,n,sum,i); 在 C 中&#xff0c;递增运算符 i 和表达式 i1 之间有显著的区别&#xff1a; i 是后置递增运算符&#xff0c;表示先使用 i 的当前值&#xff0c;然后将 i 加 1。i1 是一个简单的算术运算&#xff0c;返回 i 的当前值加 1&…...

Python功能制作之获取CSDN所有发布文章的对应数据

大家好&#xff0c;今天我要分享的是一个实用的Python脚本&#xff0c;它可以帮助你批量获取CSDN博客上所有发布文章的相关数据&#xff0c;并将这些数据保存到Excel文件中。此外&#xff0c;脚本还会为每篇文章获取一个质量分&#xff0c;并将这个分数也记录在Excel中。让我们…...

Backend - C# 基础知识

目录 一、程序结构 &#xff08;一&#xff09;内容 1. 命名空间声明 Namespace 2. 一个 class 类 3. class 方法&#xff08;类方法&#xff09; 4. class 属性 5. 一个 main 方法&#xff08;程序入口&#xff09; 6. 语句&表达式 7. 注释 &#xff08;二&#xff09;举例…...

HTML5新增的input元素类型:number、range、email、color、date等

HTML5 大幅度地增加与改良了 input 元素的种类&#xff0c;可以简单地使用这些元素来实现 HTML5 之前需要使用 JavaScript 才能实现的许多功能。 到目前为止&#xff0c;大部分浏览器都支持 input 元素的种类。对于不支持新增 input 元素的浏览器&#xff0c;input 元素被统一…...

00 Debian字符界面如何支持中文

作者&#xff1a;网络傅老师 特别提示&#xff1a;未经作者允许&#xff0c;不得转载任何内容。违者必究&#xff01; Debian字符界面如何支持中文 《傅老师Debian知识库系列之00》——原创 前言 傅老师Debian知识库特点&#xff1a; 1、拆解Debian实用技能&#xff1b; 2、…...

以太网中的各种帧结构

帧结构&#xff08;Ethernet Frame Structure&#xff09;介绍 以太网信号帧结构&#xff08;Ethernet Signal Frame Structure&#xff09;&#xff0c;有被称为以太网帧结构&#xff0c;一般可以分为两类 —— 数据帧和管理帧。 按照 IEEE 802.3&#xff0c;ISO/IEC8803-3 …...

C++入门基础题:数组元素逆序(C++版互换方式)

1.题目&#xff1a; 数组元素逆置案例描述: 请声明一个5个元素的数组&#xff0c;并且将元素逆置. (如原数组元素为:1,3,2,5,4;逆置后输出结果为:4,5,2,3,1) 2.图解思路&#xff1a; 3.代码演示&#xff1a; #include<iostream>using namespace std;int main(){int a…...

3款自己电脑就可以运行AI LLM的项目

AnythingLLM、LocalGPT和PrivateGPT都是与大语言模型&#xff08;LLM&#xff09;相关的项目&#xff0c;它们允许用户在本地环境中与文档进行交互&#xff0c;但它们在实现方式和特点上存在一些差异。AnythingLLM使用Pinecone和ChromaDB来处理矢量嵌入&#xff0c;并使用OpenA…...

各云厂商取消免费一年期SSL证书

目录 第一次削减SSL证书有效期&#xff1a; SSL证书单次签发有效期可能再次削减&#xff1a; 目前市场趋势显现&#xff1a; 各类削减政策意味着什么&#xff1a; 上有政策、下有对策—怎么实现超长SSL证书有效期&#xff1a; 如何申请全自动化部署的SSL证书&#xff1a;…...

自动化测试高级控件交互方法:TouchAction、触屏操作、点按,双击,滑动,手势解锁!

在自动化测试领域中&#xff0c;TouchAction 是一种非常强大的工具&#xff0c;它允许我们模拟用户在设备屏幕上的各种触摸事件。这种模拟不仅限于简单的点击操作&#xff0c;还包括滑动、长按、多点触控等复杂的手势。 点按与双击 点按和双击是触屏设备上最基本的操作之一。…...

leetcode165.解密数字

题目表述&#xff1a; 这道题目和斐波那契数列以及跳台阶问题十分相似。 斐波那契数列&#xff1a;0、1、1、2、3、5, 8、13、21、34 …… leetcode跳台阶问题&#xff1a;1、1、2、3、5, 8、13、21、34....... 这类题目的特点都是第N项的结果等于前两项的和。 但是解密数…...

对为什么react需要时间分片,vue3不需要的浅学习

1、时间分片 时间分片指在让应用在cpu进行大量计算时也能与用户交互&#xff0c;但时间分片只能对大量cpu计算进行优化&#xff0c;无法优化复杂DOM操作&#xff0c;因为要确保用户正在操作的界面是最新。 web卡顿的场景&#xff1a; 1、cpu计算量不大&#xff0c;但dom操作…...

电脑干货分享 · 删除资源管理器导航栏 OneDrive 及 3D 对象

Win10资源管理器的左侧导航栏默认有一个OneDrive的项&#xff0c;但由于微软龟速的原因&#xff0c;OneDrive在国内基本很少有人使用&#xff0c;我们动手给它KO了&#xff01; 网上有很多这类的教程&#xff0c;但都是要手动修改注册表&#xff0c;对于小白来说&#xff0c;有…...

无人机之穿越机注意事项篇

一、检查设备 每次飞行前都要仔细检查穿越机的每个部件&#xff0c;确保所有功能正常&#xff0c;特别是电池和电机。 二、遵守法律 了解并遵循你所在地区关于无人机的飞行规定&#xff0c;避免非法飞行。 三、评估环境 在飞行前检查周围环境&#xff0c;确保没有障碍物和…...

防御课第一次作业第一天笔记整理

网络安全概述 网络安全&#xff08;Cyber Security&#xff09;是指网络系统的硬件、软件及其系统中的数据受到保护&#xff0c;不因偶然的或者恶意的原因而遭受到破坏、更改、泄露&#xff0c;系统连续可靠正常地运行&#xff0c;网络服务不中断 中国网络安全市场近年来只增不…...

Git协作

文章目录 Git协作冲突冲突的发生情况解决冲突如何处理冲突 1 分支1.1 什么是Git分支1.2 创建分支 2 切换分支2.1 指向分支2.2 暂存分支切换分支与未提交更改的处理使用 Stash 临时保存更改Stash 的工作原理&#xff1a;场景设定使用 Git Stash 3 远程分支3.1 快进合并快进合并的…...

Three.js机器人与星系动态场景(四):封装Threejs业务组件

实际在写业务的时候不会在每个组件里都写几十行的threejs的初始化工作。我们可以 将通用的threejs的场景、相机、render、轨道控制器等进行统一初始化。同时将非主体的函数提到组件外部&#xff0c;通过import导入进组件。将业务逻辑主体更清晰一些。下面的代码是基于reactthre…...

亚马逊云科技 Amazon Bedrock 构建 AI 应用体验

前言 大模型应用发展迅速&#xff0c;部署一套AI应用的需求也越来越多&#xff0c;从头部署花费时间太长&#xff0c;然而亚马逊科技全托管式生成式 AI 服务 Amazon Bedrock&#xff0c;Amazon Bedrock 简化了从基础模型到生成式AI应用构建的复杂流程&#xff0c;为客户铺设了…...

程序员标准简历模板

链接: https://pan.baidu.com/s/1yMXGSSNba15b9hMXjA39aA?pwdb4ev 提取码: b4ev 3年工作经验简历 链接: https://pan.baidu.com/s/1OO7n1lRL6AkhejxYC9IyDA?pwdfmvv 提取码: fmvv 优秀学员简历 链接: https://pan.baidu.com/s/106Vkw_ulOInI47_5mDySSg?pwduudc 提取码: uu...

物联网设计竞赛_10_Jetson Nano中文转汉语语音

在windows中pyttsx3可以让汉字文本输出中文语音&#xff0c;但是在jetson上只能用英文说话 import pyttsx3def hanyu(test):engine pyttsx3.init()rate engine.getProperty(rate)engine.setProperty(rate,125)engine.say(test)engine.runAndWait() hanyu(你好) #engine.save…...

XML Schema 指示器

XML Schema 指示器 1. 引言 XML Schema 是一种用于定义 XML 文档结构和内容的语言。它提供了一种强大的方式来描述 XML 文档中允许的元素、属性和数据类型。XML Schema 指示器是在 XML Schema 定义中使用的一些特殊元素和属性,它们用于指示 XML 处理器如何解析和验证 XML 文…...

iOS UITableView自带滑动手势和父视图添加滑动手势冲突响应机制探索

场景 我们有时候会遇到这样的一个交互场景&#xff1a;我们有一个UITableView 放在一个弹窗中&#xff0c;这个弹窗可以通过滑动进行展示和消失&#xff08;跟手滑动的方式&#xff09;&#xff0c;然后这个UITableView放在弹窗中&#xff0c;并且可以滚动&#xff0c;展示一些…...

RAG实践:ES混合搜索BM25+kNN(cosine)

1 缘起 最近在研究与应用混合搜索&#xff0c; 存储介质为ES&#xff0c;ES作为大佬牌数据库&#xff0c; 非常友好地支持关键词检索和向量检索&#xff0c; 当然&#xff0c;支持混合检索&#xff08;关键词检索向量检索&#xff09;&#xff0c; 是提升LLM响应质量RAG(Retri…...

论文去AIGC痕迹:避免AI写作被检测的技巧

在数字化时代&#xff0c;AI正以其卓越的能力重塑学术写作的面貌。AI论文工具的兴起&#xff0c;为研究者们提供了前所未有的便利&#xff0c;但同时也引发了关于学术诚信和原创性的热烈讨论。当AI辅助写作成为常态&#xff0c;如何确保论文的独创性和个人思想的体现&#xff0…...

C#使用异步方式调用同步方法的实现方法

使用异步方式调用同步方法&#xff0c;在此我们使用异步编程模型&#xff08;APM&#xff09;实现 1、定义异步委托和测试方法 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Task…...

【Go系列】 Go语言的入门

为什么要学习Go 从今天起&#xff0c;我们将一同启程探索 Go 语言的奥秘。我会用简单明了的方式&#xff0c;逐一讲解 Go 语言的各个知识点&#xff0c;帮助你从基础做起&#xff0c;一步步深化理解。不论你之前是否有过 Go 语言的接触经验&#xff0c;这个系列文章都将助你收获…...

Dify 与 Xinference 最佳组合 GPU 环境部署全流程

背景介绍 在前一篇文章 RAG 项目对比 之后&#xff0c;确定 Dify 目前最合适的 RAG 框架。本次就尝试在本地 GPU 设备上部署 Dify 服务。 Dify 是将模型的加载独立出去的&#xff0c;因此需要选择合适的模型加载框架。调研一番之后选择了 Xinference&#xff0c;理由如下&…...