我正在使用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
代码会这样做:
- 队列状态更新将
isReady
更改为false
。 - 队列状态更新添加到
counter
。 - 队列状态更新更改
isReady
到true
。
React做这三个更新一旦你的代码已经返回,所以他们只触发一个单一的重新渲染,在重新渲染isReady
有相同的值,它有最后一次依赖于你的useEffect
被检查,所以useEffect
回调不运行。
使用作为承诺,你的代码会这样做:
- 队列状态更新将
isReady
更改为false
- 返回等待承诺解决。
- 一旦承诺解决:
- 队列状态更新添加到
counter
。 - 队列状态更新更改
isReady
到true
。
- 队列状态更新添加到
在列表中的#2和#3之间,React有机会处理状态变化,所以它确实这样做了——当isReady
是false
时触发重新渲染,这触发你的useEffect
回调,因为isReady
的值与上次检查依赖项的值不同。然后,在承诺解决后,您的代码排队其他几个状态变化,触发另一个渲染,再次触发useEffect
回调。
关键是:
- 状态更新不是立即的
- 状态更新批处理
- 即使状态改变了,然后又改变了,如果在这些变化之间没有渲染,由值不同触发的
useEffect
回调不会运行,因为值在检查时没有不同
在你的评论中问:
所以我可以实现这个行为不使用承诺?我需要
isReady
标志来控制滚动加载器,直到函数完成同步处理一些数据。
如果处理是同步的,它将占用处理UI更新的主线程,并且你的加载指示器很可能不会被动画化(如果动画是用JavaScript完成的,它肯定不会;如果它是GIF动画或类似的,它是否完成取决于浏览器以及所有帧是否已经加载;如果它是一个CSS动画,在Chrome和Firefox上的快速测试表明,它们至少会在处理发生时保持动画,但不能保证)。
如果可以避免主线程上的同步处理,我就会这样做。例如,考虑将工作发送给工作线程。
但是,要做到这一点,您需要更改isReady
,然后等待开始同步处理,直到在之后,通过等待useEffect
回调呈现更改的结果。
下面是我在这里找到的一个使用CSS旋转器的例子。
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>