React - 设置状态仅在异步操作中有效

React - Set state work only in async operation

我正在使用 React(带有 TypeScript),我发现了非常奇怪的行为。 在方法“handleAnswer”中,我正在更改变量“isReady”并使用 useEffect 挂钩打印值。但是只有当我在那里放置一个计时器时才会发生变化,当我取出计时器时,变量停止变化,并且我不再在控制台中看到输出。我做错了什么?

export const App = () => {

  const [isReady, setIsReady] = useState(true);
  const [counter, setCounter] = useState(0);


  useEffect(() => { console.log(isReady) },[isReady]);
  
  
  const handleAnswer = async (isLiked: boolean) => {
    setIsReady(false);

    // WHEN I REMOVE THIS LINE, IT DOESN'T WORK
    await new Promise(resolve => setTimeout(resolve, 10));

    setCounter(counter + 1)
    setIsReady(true);
  }


  return (
    <div className="App" >

    { isReady ? <div className="main-content"></div> :<CircularProgress /> }

    <Button variant="contained" onClick={() => handleAnswer(true)} startIcon={<ThumbUpIcon />}>
      click
    </Button>
   </div>
  );

}

export default App;

状态更新被排队并在排队它们的代码返回后作为批处理。因此,如果没有承诺,您的 handleAnswer 代码会这样做:

  1. 排队状态更新,将 isReady 更改为 false
  2. 排队添加到 counter 的状态更新。
  3. 排队状态更新,将 isReady 更改为 true

React 在您的代码返回后执行这三个更新,因此它们只触发一个 re-render,并且在此期间 re-render isReady 具有与上次相同的值检查了对 useEffect 的依赖性,因此 useEffect 回调不会 运行.

使用 承诺,您的代码将执行此操作:

  1. 排队状态更新,将 isReady 更改为 false
  2. 退还等待承诺结算。
  3. 承诺一旦确定:
    1. 排队添加到 counter 的状态更新。
    2. 排队状态更新,将 isReady 更改为 true

在该列表的第 2 和第 3 之间,React 有机会处理状态更改,它确实如此——当 isReadyfalse 时触发 re-render,这触发您的 useEffect 回调,因为 isReady 的值与上次检查依赖项时的值不同。然后稍后,在 promise 解决后,您的代码将其他几个状态更改排队,这会触发另一个渲染,再次触发您的 useEffect 回调。

重点是:

  • 状态更新不是即时的
  • 状态更新是批处理
  • 即使状态改变然后又变回,如果在这些改变之间没有渲染,由值不同触发的 useEffect 回调不会 运行,因为值检查时没有什么不同

在您提出的评论中:

So I can achieve this behavior without using promise? I need the isReady flag to control a scroll loader until the function is done handling some data synchronously.

如果处理是 同步,它会占用处理 UI 更新的主线程,并且您的加载指示器很可能不会动画(它肯定会赢如果动画是用 JavaScript 完成的;如果是动画 GIF 或类似的,是否完成取决于浏览器以及是否已经加载了所有帧;如果是 CSS 动画,则快速使用 Chrome 进行测试,Firefox 建议它们至少 在处理过程中保持动画,但不能保证)。

如果你能避免在主线程上进行同步处理,我会的。例如,考虑将作品发送到 worker thread

不过,要做到这一点,您需要更改 isReady,然后等待开始同步处理,直到 呈现该更改的结果通过等待 useEffect 回调。

这是一个使用我发现的 CSS 微调器的示例 here

const { useState, useEffect } = React;

const Example = () => {
    const [isReady, setIsReady] = useState(true);

    // On click, set `isReady` to false
    const startProcessing = () => {
        setIsReady(false);
    };

    // When `isReady` becomes false, start "processing".
    // (You may want to have an instance variable [via a ref]
    // to tell you whether to actually do processing, and if
    // so what to process.)
    useEffect(() => {
        if (!isReady) {
            // Start "processing"
            const done = Date.now() + 3000;
            while (Date.now() < done) {
                // Busy wait -- NEVER DO THIS IN REAL-WORLD CODE
            }
            // Done "processing"
            setIsReady(true);
        }
    }, [isReady]);

    return <div>
        <input type="button" onClick={startProcessing} value="Start Processing (Takes 3 Seconds)" />
        <div>isReady = {String(isReady)}</div>
        {isReady ? null : <div className="spinner-loader" />}
    </div>;
};

ReactDOM.render(<Example />, document.getElementById("root"));
/*
Credit: jlong  @github
Source: https://github.com/jlong/css-spinners
*/
@-moz-keyframes spinner-loader {
  0% {
    -moz-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -moz-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@-webkit-keyframes spinner-loader {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes spinner-loader {
  0% {
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
/* :not(:required) hides this rule from IE9 and below */
.spinner-loader:not(:required) {
  -moz-animation: spinner-loader 1500ms infinite linear;
  -webkit-animation: spinner-loader 1500ms infinite linear;
  animation: spinner-loader 1500ms infinite linear;
  -moz-border-radius: 0.5em;
  -webkit-border-radius: 0.5em;
  border-radius: 0.5em;
  -moz-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
  -webkit-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
  box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
  display: inline-block;
  font-size: 10px;
  width: 1em;
  height: 1em;
  margin: 1.5em;
  overflow: hidden;
  text-indent: 100%;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>