我在 react 中使用钩子制作了一个小的秒表组件。这是演示问题的最小代码。
查看名为resetTicks
的函数。它有两个setTicks
和setTicking
的设置器,只有setTicking
工作,即时钟被暂停,有趣的是,如果我再次单击按钮,它才会重置时钟。我尝试对两个设置器的调用重新排序,但无济于事。
const StopWatch = () => {
const [ticks,setTicks] = useState(0);
const [ticking,setTicking] = useState(false);
useEffect(() => {
setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
// these two setters are causing the issue
// only the setTicking is actually showing effect. I have tried switching
// their order but nothing works.
setTicking(false);
setTicks(0);
}
const min = Math.floor(ticks / 6000);
const sec = Math.floor((ticks - (min * 6000)) / 100);
const centis = ticks % 100;
return (
<WatchWrapper>
<WatchDisplay>
<span>{min < 10 ? '0': ''}{min}</span>
<span>:</span>
<span>{sec < 10 ? '0': ''}{sec}</span>
<span>:</span>
<span>{centis < 10 ? '0' : ''}{centis}</span>
</WatchDisplay>
<WatchControls>
<WatchBtn onClick={toggleTicking}>
{ticking ? 'stop' : 'play_arrow'}
</WatchBtn>
<WatchBtn onClick={resetTicks}>refresh</WatchBtn>
</WatchControls>
</WatchWrapper>
)
}
这是一个棘手的问题,你应该从console.log
中了解发生了什么:
true
56
true
57
true
58
true
59
false
0
false
60
它确实设置为 0,但显然在某个时候,预定的旧setTimeout
会触发,当它为 60 时,它的旧 tick 值关闭,因此它会将其重置回它。
增加超时,说 3 秒做一个console.log(ticking, ticks)
在渲染中,问题应该对您来说更明显。
这是因为setTicks
setter 和setTimeout
内部异步调用回调之间的竞争条件。setTicks
设置器更新即时报价计数,但旧的即时报价计数已存储在setTimeout
范围内。因此,setTimeout
会引发回调,将其旧值ticks
作为参数。您需要清理组件卸载时的setTimeout
,以防止出现以下情况:
useEffect(() => {
const timeout = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
}, 10);
return () => clearTimeout(timeout);
}, [ticking, ticks]);
为了确保没有竞争条件,你可以尝试通过为计时器创建一个 React 引用来重置 setTimeout,然后再重置 resetTicks。
const [ticks,setTicks] = React.useState(0);
const [ticking,setTicking] = React.useState(false);
const timer = React.createRef();
React.useEffect(() => {
timer.current = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking, timer]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
clearTimeout(timer.current);
setTicking(false);
setTicks(0);
}
在这里使用代码沙盒进行测试:
https://codesandbox.io/embed/test-reset-race-condition-g53l5?fontsize=14&hidenavigation=1&theme=dark&view=editor
useEffect(() => {
const interval = setInterval(() => {
if (ticking) setTicks(prevState => prevState + 1);
}, 10);
return () => clearInterval(interval);
}, [ticking]);