setState不会立即在setInterval

时间:2020-06-11 15:39:42

标签: javascript reactjs

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

以下是代码框: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)

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

我该如何解决?

1 个答案:

答案 0 :(得分:0)

这里存在一个基本的设计问题,即setInterval是错误的节省时间的工具。 setInterval仅保证回调不会在至少 1000毫秒内运行,而不能保证在确切的 1000毫秒内运行,因此您将随着漂移和跳跃时间而结束。

我建议使用requestAnimationFrameDate库来确定何时出现滴答声。

通过此设置,您可以使用以下方法按比例调整时针:小时剩余的分钟数。

(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();
  const prevDateRef = useRef();
  
  const tick = () => {
    const now = new Date();
    
    if (prevDateRef.current && 
        now.getSeconds() !== prevDateRef.current.getSeconds()) {
      setDate(now);
    }
    
    prevDateRef.current = 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/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

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

.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 speedMS = 5;

class MockDate {
  static second = 0;
  static minute = 0;
  static hour = 0;
  
  constructor() {
    this.second = MockDate.second;
    this.minute = MockDate.minute;
    this.hour = MockDate.hour;
  }
  
  getSeconds() {
    return this.second;
  }
  
  getMinutes() {
    return this.minute;
  }
  
  getHours() {
    return this.hour || 12;
  }
}

setInterval(() => {
  if (++MockDate.second === 60) {
    MockDate.second = 0;

    if (++MockDate.minute === 60) {
      MockDate.minute = 0;
      MockDate.hour = (MockDate.hour + 1) % 12;
    }
  }
}, speedMS);

const Clock = () => {
  const [date, setDate] = useState(new MockDate());
  const requestRef = useRef();
  const prevDateRef = useRef();
  const tick = () => {
    const now = new MockDate();
    
    if (prevDateRef.current && 
        now.getSeconds() !== prevDateRef.current.getSeconds()) {
      setDate(now);
    }
    
    prevDateRef.current = 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/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>