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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 前端工程化(01):10款自动化构建工具初识。
  • [GHCTF 2024 新生赛]ezzz_unserialize
  • 攻防世界 Web_python_template_injection(flask模版注入)
  • 网络安全应急响应信息收集利器-Eagle_Eye
  • Java中Collection集合和Map集合详解(进阶三)
  • sql注入之宽字节注入
  • WEB攻防-通用漏洞SQL注入-ACCESS一般注入与偏移注入
  • 【Scrapy】深入了解 Scrapy 中间件中的 process_spider_output 方法
  • Android 注解的语法原理和使用方法
  • 软设之代理模式
  • 【国内超大型智能算力中心建设白皮书 2024】_智算中心算力规划
  • lodop使用教程---ivx
  • docker 基础命令
  • 免费听书TV版v1.0.1
  • 31. 1049. 最后一块石头的重量 II, 494.目标和,474.一和零
  • .pyc 想到的一些问题
  • 《Java编程思想》读书笔记-对象导论
  • JAVA 学习IO流
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • javascript从右向左截取指定位数字符的3种方法
  • JavaScript设计模式与开发实践系列之策略模式
  • orm2 中文文档 3.1 模型属性
  • Python3爬取英雄联盟英雄皮肤大图
  • session共享问题解决方案
  • Transformer-XL: Unleashing the Potential of Attention Models
  • 阿里中间件开源组件:Sentinel 0.2.0正式发布
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 如何使用 JavaScript 解析 URL
  • 思否第一天
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • (+4)2.2UML建模图
  • (30)数组元素和与数字和的绝对差
  • (不用互三)AI绘画:科技赋能艺术的崭新时代
  • (超详细)语音信号处理之特征提取
  • (代码示例)使用setTimeout来延迟加载JS脚本文件
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (黑马点评)二、短信登录功能实现
  • (理论篇)httpmoudle和httphandler一览
  • (免费领源码)Python#MySQL图书馆管理系统071718-计算机毕业设计项目选题推荐
  • (算法)硬币问题
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (文章复现)基于主从博弈的售电商多元零售套餐设计与多级市场购电策略
  • (小白学Java)Java简介和基本配置
  • (一)Java算法:二分查找
  • (一)UDP基本编程步骤
  • (一)十分简易快速 自己训练样本 opencv级联haar分类器 车牌识别
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • (转) RFS+AutoItLibrary测试web对话框
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .NET CORE 第一节 创建基本的 asp.net core
  • .net core 3.0 linux,.NET Core 3.0 的新增功能
  • .net core webapi 大文件上传到wwwroot文件夹
  • .net core 连接数据库,通过数据库生成Modell
  • .Net 路由处理厉害了