React hook useState 的异步引起的bug

发布于 2022-09-30 23:11:28 字数 1514 浏览 27 评论 0

线上代码参考:
https://codepen.io/hellomrbig...

使用 react hook 做了一个类似 tooltip 的组件。将鼠标放在灰色块上会显示蓝色块。但是现在有一个问题,灰色块和蓝色块中间有一个间隙,鼠标移动到这个间隙的时候蓝色块会消失。为此我在鼠标移除的方法中添加了一个延时函数,并且加了一个标识符 visible2

const handleMouseLeave = (type) => {
    console.log('leave', type)
    if (type === 'main') {
      setTimeout(() => {
        console.log('visible', visible, 'visible2', visible2)
        if (!visible2 && visible) { // visible2 用来标识鼠标是否移动到蓝色块内
          setVisible(false)
        }
      }, 1000)
    } else if (type === 'content') {
      // visible2 = false
      setVisible2(false)
      setVisible(false)
    }
  }

事件的触发顺序应该是 离开灰色块 -> 进入蓝色块 -> setVisible2(true) 这时才触发灰色块 handleMouseLeave 中的这段代码,此时 visible2 已经是 true 了,所以不会触发 setVisible(false),导致蓝色块消失。

console.log('visible', visible, 'visible2', visible2)
if (!visible2 && visible) { // visible2 用来标识鼠标是否移动到蓝色块内
  setVisible(false)
}

但是在功能演示时发现鼠标移动到蓝色块中后过了延时时间蓝色块还是消失了。。。并且console.log('visible', visible, 'visible2', visible2)打印出来的结果是
visible true visible2 false 说明触发了蓝色块中的 handleMouseEnter 方法中的 setVisible2(true)visible2 还是 false。我就有点懵,虽然 setVisible2 是异步函数,但是我的延时函数应该会在它结束后执行。所以这里是不是我对 useState 的理解有点问题。
解决方法是将标识符 visible2let 声明(参考我注释的代码)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

暗恋未遂 2022-10-07 23:11:28

(1)原因:

setState会导致Function组件的重新执行,但是setTimeout捕获的变量是前一轮更新的,所以visible2会一直为false,所以导致代码的行为不符合预期详细的原因我在这个回答中有解释

(2)解决方法:

如果就是要使用useState保存状态visible2,而且要使代码行为符合预期那么只需要让setTimeout第一个函数中捕获的变量是新一轮更新的即可,所以我们可以对代码做以下更改在线体验地址

import { useState, useEffect, useRef } from "react";

function App() {
  ...
  const closePopover = () => {
    console.log("visible", visible, "visible2", visible2);
    if (!visible2 && visible) {
      setVisible(false);
    }
  };

  latestFunc.current = closePopover;
  
  const handleMouseLeave = (type) => {
    console.log("leave", type);
    if (type === "main") {
      setTimeout(() => {
        latestFunc.current();
      }, 1000);
    } else if (type === "content") {
      // visible2 = false
      setVisible2(false);
      setVisible(false);
    }
  };

  ...
}

这样的话setTimeout中执行的函数捕获的变量就是最新的了,因为组件每次重新执行是我们都把
latestFunc.current指向到了新创建的函数,更多的相关问题可以看我的这个回答

(3) 为什么把visible2放在let声明的普通变量中就能正常呢

如果前面的东西你都看了的话,那么你就会知道setVisible2会导致函数组件的重新执行,进而导致setTimeout中函数捕获的变量出现问题,而直接给普通变量赋值不会导致重新执行函数组件,就不会出现这种问题,所以等到setTimeout执行时,他其中捕获的变量并不会出现什么问题

歌枕肩 2022-10-07 23:11:28

把visible2换成ref把(虽然感觉这个需求在css上动动手脚好像也可以

吹梦到西洲 2022-10-07 23:11:28

鼠标进入蓝色区域 需要把定时器清掉

梅窗月明清似水 2022-10-07 23:11:28

想复杂了吧,延迟隐藏,进入就清掉延迟器

const { useState, useEffect } = React

function App () {
  const [visible, setVisible] = useState(false)
  let timer = null
  const handleMouseEnter = (type) => {
    if (type === 'content') {
      clearTimeout(timer)
    } else setVisible(true)
  }
  const handleMouseLeave = (type) => {
    if (type === 'main') {
      timer = setTimeout(() => {
        if (visible) {
          setVisible(false)
        }
      }, 1000)
    } else if (type === 'content') {
      setVisible(false)
    }
  }
  return (
    <div>
      <div
        class="content1"
        onMouseEnter={() => handleMouseEnter('main')}
        onMouseLeave={() => handleMouseLeave('main')}
       >touch me</div>
      <div
        className={['content2', visible && 'visible' || null].join(' ')}
        onMouseEnter={() => handleMouseEnter('content')}
        onMouseLeave={() => handleMouseLeave('content')}
      >content</div>
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
‘画卷フ 2022-10-07 23:11:28

这是一个闭包问题!
image.png

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文