setState 不会在 setInterval 内立即更新状态

IT技术 javascript reactjs
2021-05-21 21:32:56

我正在尝试构建一个模拟时钟,秒针每秒旋转一次,分针每分钟旋转 6 度,时针每 12 分钟旋转 6 度。

这是代码和框:https ://codesandbox.io/s/react-example-d7mes ? file =/ Clock.js

每当分针的角度是其中之一[72, 144, 216, 288, 360](12 分度)时,我将时针旋转 6 度一次。

这就是我正在做的:

 let twelveMinDegrees = [72, 144, 216, 288, 360];

 setInterval(() => {
        this.setState(prev => ({
            sec: prev.sec == 360 ? 6 : prev.sec + 6,  //degrees
            min: prev.sec == 354 ? (prev.min == 360 ? 6 : prev.min + 6) : prev.min,  //degrees
            hrs: (function(){  //degrees

                const indx = twelveMinDegrees.findIndex(el => el == prev.min)
                if(!minChanged && indx >=0){ //only change once for every 12min
                    minChanged = true; 
                    let incHrs = prev.hrs + (6*indx);
                    console.log(incHrs);
                    return incHrs; 
                }else{
                    if(!twelveMinDegrees.includes(prev.min)){
                        minChanged = false;
                    }
                    return prev.hrs;
                }
            })()
         }))
    }, 1000)

但是时针没有改变,第二次被设置回else部分的前一个值并且返回的值incHrs被忽略,因为在状态更新之前,else被称为下一秒返回prev.hrs的仍然是旧值(不是if(!minChanged && indx >=0)) 中返回的值

我该如何解决?

1个回答

这里有一个基本的设计问题,那就是这setInterval是错误的计时工具。setInterval只保证回调不会运行至少1000 毫秒,而不是它会1000 毫秒内运行,所以你会以漂移和跳过时间结束。

我建议使用requestAnimationFrameDate库或performance.now确定发生滴答的时间。

通过此设置,您可以按比例缩放时针与一小时剩余的分钟数:

(hours + minutes / 60) * 30 + 180

如果您想要更粗略的时针调整粒度,请将分钟截断为 6 个不同的块:

(hours + floor(minutes / 10) * 10 / 60) * 30 + 180

在数学上这样做比在硬编码数组中查找增量点要简单得多。

这是一个最小的示例,您可以使用它来保持准确的时间(我将样式留给您):

.hand {
  width: 2px;
  height: 40%;
  background-color: black;
  transform-origin: top center;
  position: absolute;
  border-radius: 3px;
  top: 50%;
  left: 50%;
}

.analog-clock {
  position: relative;
  border-radius: 50%;
  border: 1px solid #aaa;
  height: 120px;
  width: 120px;
}
<script type="text/babel" defer>
const {Fragment, useEffect, useState, useRef} = React;

const Clock = () => {
  const [date, setDate] = useState(new Date());
  const requestRef = useRef();
  let prevDate = null;
  
  const tick = () => {
    const now = new Date();
    
    if (prevDate && now.getSeconds() !== prevDate.getSeconds()) {
      setDate(now);
    }
    
    prevDate = now;
    requestRef.current = requestAnimationFrame(tick);
  };
  
  useEffect(() => {
    requestRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
  
  const pad = n => n.toString().padStart(2, 0);
  
  const computeHourDeg = date => 
    (date.getHours() + ~~(date.getMinutes() / 10) * 10 / 60) * 30 + 180
  ;

  return (
    <Fragment>
      <div className="analog-clock">
        <div
          className="hand"
          style={{transform: `rotate(${6 * date.getSeconds() + 180}deg)`}}
        ></div>
        <div
          className="hand"
          style={{transform: `rotate(${6 * date.getMinutes() + 180}deg)`}}
        ></div>
        <div
          className="hand"
          style={{background: "red", 
                  height: "30%",
                  transform: `rotate(${computeHourDeg(date)}deg)`}}
        ></div>
      </div>
      <h3>
        {pad(date.getHours())}:
        {pad(date.getMinutes())}:
        {pad(date.getSeconds())}
      </h3>
    </Fragment>
  );
};

ReactDOM.render(<Clock />, document.body);

</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

这是一个带有模拟Date对象的加速版本,以说明它可以正常工作: