令人困惑的React useEffect钩子行为

3

我是React的新手,正在学习useEffect hooks。在完成hooks课程后,我正在进行练习。

我有一个按钮来切换arrowState状态的变化。

export default function Test() {
  const [arrowState, setArrowState] = useState(true);
  const [arrowChange, setArrowChange] = useState(false);

  
  const toggleButton = () => {
    return setArrowState((arrowState) => {
      return !arrowState;
    });
  };

  return (
    <>
      <div>
        <button
          type="button"
          onClick={toggleButton}
        >
          Toggle Arrow
        </button>
      </div>
      {arrowChange && <Notific />}
    </>
  );
}

然后我有一个 useEffect 钩子,用于改变 arrowChange 的状态。同时设置了一个计时器,在 2 秒后将 arrowChange 的状态改为 false,从而自动隐藏该按钮。

useEffect(() => {
    console.log("first line of useEffect.arrowChange is ", arrowChange);
    setArrowChange(true);
    console.log(
      "immediately after setArrowChange to true. arrowChange is ",
      arrowChange
    );

    setTimeout(() => {
      console.log("first line of setTimeOut. arrowChange is ", arrowChange);
      arrowChange && setArrowChange(false);
    }, 1000);

    return () => {
      console.log("cleaning up arrowchange. arrowChange is ", arrowChange);
      setArrowChange(false);
    };
  }, [arrowState]);

基于此,我将显示另一个带有消息的按钮。

function Notific() {
    return (
      <button
        type="button"
        onClick={() => setArrowChange(false)}
      >
        Arrow state has been changed
      </button>
    );
  }

如您所见,我正在记录一些信息以查看效果。令人惊讶的是,即使在 useEffect hook 开始时将 arrowChange 设置为 true,在交替事件中它仍然显示为 false。

请查看这些控制台日志:

cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

Test.js:70 cleaning up arrowchange. arrowChange is  true
Test.js:57 first line of useEffect.arrowChange is  false
Test.js:59 immediately after setArrowChange to true. arrowChange is  false
Test.js:65 first line of setTimeOut. arrowChange is  false

Test.js:70 cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

Test.js:70 cleaning up arrowchange. arrowChange is  true
Test.js:57 first line of useEffect.arrowChange is  false
Test.js:59 immediately after setArrowChange to true. arrowChange is  false
Test.js:65 first line of setTimeOut. arrowChange is  false

Test.js:70 cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

2
  1. 状态更新是异步的。
  2. 在组件的特定渲染中,状态是不变的。
  3. 组件在重新渲染之前无法看到更新后的状态。
  4. useEffect 钩子的回调函数对状态有闭包。
- Yousaf
谢谢您的评论,但对于像我这样的初学者来说,这太过理论化了。我不理解。 - chief dot101
React会批处理所有的useState更新并异步地一次性提交它们。但是console.log在遇到时会打印出该值,并且不会等待React更新状态后再记录信息。 - Sushanth --
请参见:https://github.com/streamich/react-use/blob/master/docs/useLatest.md - AKX
1个回答

4
为了理解代码的输出,您需要了解React如何更新状态以及闭包的概念。
状态更新是异步的
每次调用状态设置函数(例如setArrowChange)都会被安排 - 状态不会立即更新。
因此,在调用setArrowChange之后立即记录arrowChange的值将记录arrowChange的旧值。
状态在组件的特定渲染中是常量
状态是恒定的;这意味着无论您调用状态设置函数多少次,组件都不能看到更新后的状态,直到重新渲染。
setTimeout的回调函数对状态具有闭包
当您在JavaScript中创建一个函数时,它会形成对其周围范围的闭包
在您的情况下,setTimeout 的回调函数将记录创建回调函数时 arrowChange 的值。
如果在调用 setTimeout 时,arrowChangefalse,并且在计时器到期之前,您将状态更新为 true,则由于 闭包,回调函数仍将记录 falseuseEffect 钩子的清理函数也对状态的 arrowChange 有一个 闭包;它记录了在创建并从 useEffect 钩子的回调函数中返回清理函数时生效的 arrowChange 的值。
现在让我们看一下第一组 console.log 输出。

第一组:

cleaning up arrowchange. arrowChange is false 
Test.js:57 first line of useEffect.arrowChange is true 
Test.js:59 immediately after setArrowChange to true. arrowChange is true
Test.js:65 first line of setTimeOut. arrowChange is true

输出的第一行

cleaning up arrowchange. arrowChange is  false

这段代码是 useEffect 钩子函数的清除函数:

  • 在再次运行 useEffect 之前
  • 在组件卸载之前

如上所述,它记录了 arrowChange 的值,该值在创建 useEffect 钩子函数的清除函数时被捕获。清除函数的输出表明:

  1. 在创建清除函数并从 useEffect 钩子的回调函数中返回时,arrowChange 的值为 false
  2. arrowChange 的初始值为 false,因此这个输出来自于清除函数,很可能是在组件的初始渲染后,第一次执行 useEffect 钩子时创建的

接下来三行

Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

记录arrowChange的最新值,这很可能是由于在useEffect钩子的第二行调用了setArrowChange(true)。该钩子在组件的初始渲染后执行。

如果观察其他组console.log输出的结果,您将注意到清除函数记录的arrowChange值与useEffect钩子回调函数中console.log语句记录的值相反。

原因是清除函数来自useEffect钩子的前一个执行,并记录了前一次执行useEffect钩子期间有效的arrowChange值。

useEffect钩子的回调函数内,console.log语句记录了arrowChange的最新值,在下一次执行useEffect钩子时,这个最新值将成为上一个值,而这个最新值的相反值将成为最新值

讲解得非常清楚。我现在理解了渲染时对周围范围的闭包概念。现在完全明白了。感谢您的努力。 - chief dot101

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接