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]); 

........
<Button
 size="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.mfbz.cn/a/784763.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

js使用proxy代理监听控制事件

本文为proxy代理的实例应用&#xff0c;有关代理的内容可以参考&#xff1a; js语法---理解反射Reflect对象和代理Proxy对象 监听事件 要监听dom元素的事件&#xff0c;我们会采用回调触发的方式来执行操作&#xff0c; 而触发事件的过程很明显是一个异步操作&#xff0c;异…

Oracle中EXIT Statement用于终止循环语句的关键字

Oracle的EXIT Statement是PL/SQL编程语言中用于终止循环语句的关键字。它有两种主要形式&#xff1a;无条件EXIT和条件EXIT WHEN。以下是对Oracle EXIT Statement的详细解释&#xff1a; 1. 无条件EXIT 语法&#xff1a;EXIT; 作用&#xff1a;无条件地终止当前循环。当程序执…

【咨询】企业数字档案馆(室)建设方案-模版范例

导读&#xff1a;本模版来源某国有大型医药行业集团企业数字档案馆&#xff08;室&#xff09;建设方案&#xff08;一期300W、二期250W&#xff09;&#xff0c;本人作为方案的主要参与者&#xff0c;总结其中要点给大家参考。 目录 1、一级提纲总览 2、项目概述 3、总体规…

办公必备——ONLYOFFICE8.1版本桌面编辑器

一、介绍ONLYOFFICE ONLYOFFICE是一款免费的开源办公软件&#xff0c;它可以让你创建、编辑和分享文档、表格和演示文稿。就像微软的Office一样&#xff0c;但它是完全免费的&#xff0c;而且可以在多种设备上使用&#xff0c;包括电脑和手机。它还支持多人同时在线编辑文档&am…

SpringCloud 负载均衡

目录 一、负载均衡 1、问题 2、什么是负载均衡 服务端负载均衡 客户端负载均衡 二、Spring Cloud LoadBalance 1、使用 Spring Cloud LoadBalance 2、负载均衡策略 3、LoadBalancer 原理 一、负载均衡 1、问题 我们来看一下前面写的代码&#xff1a; List<Serv…

抖音矩阵云剪系统saas源码 短视频矩阵获客管理系统

2024抖音矩阵云混剪系统是一款专业的短视频营销管理工具。该系统支持多平台多账号的集中式管理&#xff0c;并实现一键式作品发布功能。它配备了智能标题生成和关键词优化工具&#xff0c;以及排名查询机制&#xff0c;帮助用户提升内容在平台上更好的矩阵管理. 智能剪辑 托管发…

java中 使用数组实现需求小案例

Date: 2024.04.08 18:32:57 author: lijianzhan 需求实现&#xff1a; 设计一个java类&#xff0c;java方法&#xff0c;根据用户手动输入的绩点&#xff0c;从而获取到绩点最高的成绩。 实现业务逻辑的代码块 import java.util.Scanner;public class PointDemo {/*** 需求&…

Python 如何实现数据驱动的接口自动化测试

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 大家在接口测试的过程中&#xff0c;很多时候会用到对CSV的读取操作&#xff0c;本文主要说明Pyt…

【Java]认识泛型

包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个基本类型都对应了一个包装类型。 除了 Integer 和 Character&#xff0c; 其余基本类型的包装类都是首字母大写。 泛型 泛型是在JDK1.5引入的…

spring boot 3.x版本中集成spring security 6.x版本进行实现动态权限控制解决方案

一、背景 最近在进行项目从jdk8和spring boot 2.7.x版本技术架构向jdk17和spring boot 3.3.x版本的代码迁移&#xff0c;在迁移过程中&#xff0c;发现spring boot 3.3.x版本依赖的spring security版本已经升级6.x版本了&#xff0c;语法上和spring security 5.x版本有很多地方…

AntV X6 图编辑引擎速通

前言&#xff1a;参考 [AntV X6 官网](https://x6.antv.antgroup.com/) 一、简介 X6 可以快速搭建 DAG 图、ER 图、流程图、血缘图等应用。 二、快速上手 1. 安装 npm install antv/x6 --save# oryarn add antv/x6# orpnpm add antv/x6 2. 使用 2.1 初始画布 在页面中创…

【Linux进阶】文件系统3——目录树,挂载

前言 在Windows 系统重新安装之前&#xff0c;你可能会事先考虑&#xff0c;到底系统盘C盘要有多大容量&#xff1f;而数据盘D盘又要给多大容量等&#xff0c;然后实际安装的时候&#xff0c;你会发现其实C盘之前会有个100MB的分区被独立出来&#xff0c;所以实际上你就会有三个…

Java进阶----继承

继承 一.继承概述 继承是可以通过定义新的类&#xff0c;在已有类的基础上扩展属性和功能的一种技术. 案例&#xff1a;优化 猫、狗JavaBean类的设计 狗类&#xff1a;Dog 属性&#xff1a;名字 name&#xff0c;年龄 age 方法&#xff1a;看家 watchHome()&#xff0c;Gett…

QT5.12.9 通过MinGW64 / MinGW32 cmake编译Opencv4.5.1

一、安装前准备&#xff1a; 1.安装QT,QT5.12.9官方下载链接&#xff1a;https://download.qt.io/archive/qt/5.12/5.12.9/ QT安装教程&#xff1a;https://blog.csdn.net/Mark_md/article/details/108614209 如果电脑是64位就编译器选择MinGW64&#xff0c;32位就选择MinGW…

C#描述-计算机视觉OpenCV(5):直方图算法

C#描述-计算机视觉OpenCV&#xff08;5&#xff09;&#xff1a;直方图算法 前文链接图像直方图灰度直方图的计算灰度直方图的绘制BGR三通道的直方图直方图的均衡化算法相似图像检测 前文链接 文中没提到的东西&#xff0c;很可能都在前文描述过 C#描述-计算机视觉OpenCV&…

【C++深度探索】继承机制详解(二)

hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;大耳朵土土垚的博客 &#x1…

深入解析:抖音视频标题的Python爬虫提取方法

引言 随着短视频的兴起&#xff0c;抖音已经成为全球最受欢迎的社交媒体平台之一。对于数据分析师、市场研究人员以及内容创作者来说&#xff0c;能够从抖音上抓取数据是一项宝贵的技能。本文将深入解析如何使用Python编写爬虫程序来提取抖音视频的标题。 爬虫基础 在开始编…

K8S 上部署大数据相关组件

文章目录 一、前言二、Redis 一、前言 Artifact Hub 是一个专注于云原生应用的集中式搜索和发布平台。它旨在简化开发者在 CNCF&#xff08;Cloud Native Computing Foundation&#xff09;项目中寻找、安装和分享包与配置的过程。用户可以通过这个平台方便地发现、安装各类云原…

为什么需要重写equals和如何重写equals

首先先看Java中的 &#xff0c;比较的两个对象的地址值。 如果是基本数据类型&#xff0c;那么就是比较的是值。 如果是引用数据类型&#xff0c;比较的就是地址. object类中的equals方法也是用的&#xff1b; 所以要比较两个对象的大小&#xff0c;去调用默认的equals方法…

Apache Spark分布式计算框架架构介绍

目录 一、概述 二、Apache Spark架构组件栈 2.1 概述 2.2 架构图 2.3 架构分层组件说明 2.3.1 支持数据源 2.3.2 调度运行模式 2.3.3 Spark Core核心 2.3.3.1 基础设施 2.3.3.2 存储系统 2.3.3.3 调度系统 2.3.3.4 计算引擎 2.3.4 生态组件 2.3.4.1 Spark SQL 2.…