React hook useState 的异步引起的bug

线上代码参考:
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 声明(参考我注释的代码)。

阅读 5.2k
5 个回答

(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执行时,他其中捕获的变量并不会出现什么问题

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

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

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

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

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

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题