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

news2024/9/17 8:29:13

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.coloradmin.cn/o/1908919.html

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

相关文章

项目机会:4万平:智能仓,AGV,穿梭车,AMR,WMS,提升机,机器人……

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 如下为近期国内智能仓储物流相关项目的公开信息线索&#xff0c;这些项目具体信息会发布到知识星球&#xff0c;请感兴趣的球友先人一步到知识星球【智能仓储物流技术研习社】自行下载…

LeetCode热题100刷题9:25. K 个一组翻转链表、101. 对称二叉树、543. 二叉树的直径、102. 二叉树的层序遍历

25. K 个一组翻转链表 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), nex…

Java | Leetcode Java题解之第223题矩形面积

题目&#xff1a; 题解&#xff1a; class Solution {public int computeArea(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2) {int area1 (ax2 - ax1) * (ay2 - ay1), area2 (bx2 - bx1) * (by2 - by1);int overlapWidth Math.min(ax2, bx2) -…

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

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

C++相关概念和易错语法(16)(list)

1.list易错点 &#xff08;1&#xff09;慎用list的sort&#xff0c;list的排序比vector慢得多&#xff0c;尽管两者时间复杂度一样&#xff0c;甚至不如先把list转为vector&#xff0c;用vector排完序后再转为list &#xff08;2&#xff09;splice是剪切链表&#xff0c;将…

AGAST (角点检测)

AGAST检测原理 AGAST(Adaptive and Generic Accelerated Segment Test)算法是Elmar于2010年提出的特征检测算法,改进了FAST(Features from Accelerated Segment Test)特征检测方法,使其具有更快的速度和更好的鲁棒性。AGAST算法提供了比FAST算法更详细的特征标记方式和判断依…

读人工智能全传07智能体

1. 布鲁克斯革命 1.1. 随着科学认知的发展&#xff0c;有时候旧有科学体系会面临全盘崩溃的危机&#xff0c;新的科学体系诞生&#xff0c;取代传统的、既定的科学体系&#xff0c;这就意味着科学的范式将发生变化 1.2. 澳大利亚机器人学家罗德尼布鲁克斯(Rodney Brooks)&…

vue3+ el-tree 展开和折叠,默认展开第一项

默认第一项展开: 展开所有项&#xff1a; 折叠所有项&#xff1a; <template><el-treestyle"max-width: 600px":data"treeData"node-key"id":default-expanded-keys"defaultExpandedKey":props"defaultProps"…

Qt creator 控件转到槽 报错 The class containing “Ui:Dialog“ could not be found in

今天调试程序&#xff0c;发现主界面控件转到槽&#xff0c;报错如下图&#xff1a; 问题表现为&#xff1a;只有主窗口控件有这个错误&#xff0c;其他子窗口正常。 解决&#xff1a; 在网上搜这个报错信息&#xff0c;都没有一个很好的解决办法。 最后发现是我在子窗口中要…

004-基于Sklearn的机器学习入门:回归分析(下)

本节及后续章节将介绍机器学习中的几种经典回归算法&#xff0c;包括线性回归&#xff0c;多项式回归&#xff0c;以及正则项的岭回归等&#xff0c;所选方法都在Sklearn库中聚类模块有具体实现。本节为下篇&#xff0c;将介绍多项式回归和岭回归等。 2.3 多项式回归 在一般的…

Visual Studio Code 教程 VsCode安装Live Server以服务形式打开html

搜索Live Server 插件,然后安装 选一个html文件&#xff0c;右键点击 Open with live server,然后就自动弹出来了

怎样优化 PostgreSQL 中对日期时间范围的模糊查询?

文章目录 一、问题分析&#xff08;一&#xff09;索引未有效利用&#xff08;二&#xff09;日期时间格式不统一&#xff08;三&#xff09;复杂的查询条件 二、优化策略&#xff08;一&#xff09;使用合适的索引&#xff08;二&#xff09;规范日期时间格式&#xff08;三&a…

北森锐途人才竞聘盘点管理测评:高管领导力六大评判标准深度解析万达商管中国绿发等

北森锐途人才管理测评&#xff1a;高管领导力评判标准深度解析 在企业高管的盘点与竞聘测评领域&#xff0c;众多管理人才面临评估自身领导力的挑战。面对能力卓越、职级显赫的同僚&#xff0c;许多管理者感到缺乏一套权威且专业的评价体系。然而&#xff0c;无论是天赋异禀的领…

Html5前端基本知识整理与回顾上篇

今天我们结合之前上传的知识资源来回顾学习的Html5前端知识&#xff0c;与大家共勉&#xff0c;一起学习。 目录 介绍 了解 注释 标签结构 排版标签 标题标签 ​编辑 段落标签 ​编辑 换⾏标签 ​编辑 ⽔平分割线 ⽂本格式化标签 媒体标签 绝对路径 相对路径 …

Chromium编译指南2024 Linux篇-安装官方工具depot_tools(二)

1.引言 在上一节中&#xff0c;我们已经完成了 Git 的安装&#xff0c;并了解了其在 Chromium 编译过程中的重要性。接下来&#xff0c;我们将继续进行环境的配置&#xff0c;首先是安装和配置 Chromium 编译所需的重要工具——depot_tools。 depot_tools 是一组用于获取、管…

怎样优化 PostgreSQL 中对布尔类型数据的查询?

文章目录 一、索引的合理使用1. 常规 B-tree 索引2. 部分索引 二、查询编写技巧1. 避免不必要的类型转换2. 逻辑表达式的优化 三、表结构设计1. 避免过度细分的布尔列2. 规范化与反规范化 四、数据分布与分区1. 数据分布的考虑2. 表分区 五、数据库参数调整1. 相关配置参数2. 定…

linux工具应用_GVIM

gvim 1. introduction1.1 **gvim的功能(选择用gvim的原因)**1.2 模式及切换1.2.1 normal1.2.2 insert1.2.3 visual1.2.4 command2. gvim配置-vimrc2.1 什么是vimrc2.2 配置修改及理解2.2.1 基本修改2.2.2 UI 相关配置2.2.3 编码相关配置2.3.4 文件相关配置2.3.5 编辑器相关配…

用Conda配置 Stable Diffusion WebUI 1.9.4

用Conda配置 Stable Diffusion WebUI 1.9.4 本文主要讲解: 如何用Conda搭建Stable Diffusion WebUI 1.9.4环境&#xff0c;用Conda的方式安装&#xff0c;不需要单独去安装Cuda了。 1. 安装miniconda https://docs.anaconda.com/free/miniconda/index.html 2. 搭建虚拟环境…

Java设计模式---(创建型模式)工厂、单例、建造者、原型

目录 前言一、工厂模式&#xff08;Factory&#xff09;1.1 工厂方法模式&#xff08;Factory Method&#xff09;1.1.1 普通工厂方法模式1.1.2 多个工厂方法模式1.1.3 静态工厂方法模式 1.2 抽象工厂模式&#xff08;Abstract Factory&#xff09; 二、单例模式&#xff08;Si…

快速掌握AI的最佳途径实践

科技时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为许多人希望掌握的重要技能。对于普通人来说&#xff0c;如何快速有效地学习AI仍然是一个挑战。本文将详细介绍几种快速掌握AI的途径&#xff0c;并提供具体的操作步骤和资源建议。 前言 AI的普及和应用已经深…