当在点击处理程序中使用两个 useState 设置器时,只有一个有效

When two useState setters are being used in a click handler only one works

我在 React 中使用钩子制作了一个小秒表组件。这是演示问题的最少代码。

查看名为 resetTicks 的函数。它有两个设置器 setTickssetTicking,只有 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 是预定的火灾,它在 tick 的旧值上有一个 closure 60,因此它会将其重置为它。

增加超时说 3 秒做一个 console.log(ticking, ticks) 在渲染中,你应该更清楚问题是什么。

  useEffect(() => {
    const interval = setInterval(() => {
      if (ticking) setTicks(prevState => prevState + 1);
    }, 10);

    return () => clearInterval(interval);
  }, [ticking]);

这是因为 setTicks setter 和 setTimeout 内部回调的异步调用之间的竞争条件。 setTicks setter 更新了滴答计数,但旧的滴答计数已存储在 setTimeout 范围内。因此 setTimeout 引发回调,将 ticks 的旧值作为参数。您需要清理组件卸载时的 setTimeout 以防止:

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (ticking) setTicks(ticks + 1);
    }, 10);
    return () => clearTimeout(timeout);
  }, [ticking, ticks]);

为确保不存在竞争条件,您可以尝试通过为计时器创建一个 React 引用来在 resetTicks 之前重置 setTimeout。

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

在此处使用 Codesandbox 进行测试:

https://codesandbox.io/embed/test-reset-race-condition-g53l5?fontsize=14&hidenavigation=1&theme=dark&view=editor