setTimeout和setInterval實現倒計時的區別

前言

這是一個因爲倒計時插件出現bug而出現的文章,致使我努力去尋找這個緣由的源頭,最後終於發現了新大陸(先事先展現一下新大陸的結論):react

  1. setTimeout和setInterval都有偏差
  2. 以1秒爲例,setInterval會每次準時在1秒鐘的時候將微任務推入主任務隊列,致使若是某次本該在時間(1s=1000ms)1998ms的時候改變數據,可是變成2000ms的時候改變數據。它(setInterval)在下一次循環的2998ms依然會改變數據,以此類推,致使此時會展現(這裏考慮省略小數點,四捨五入一樣原理):2000ms(2s)->2998ms(2s)->3998ms(3s)->4998ms(4s)->6000ms(6s)->6998ms(6s)
  3. 以1秒爲例,setTimeout會每次都在1秒鐘後將微任務推入主任務隊列,致使若是某次本該在時間(1s=1000ms)1998ms的時候改變數據,以此類推,致使此時會展現(這裏考慮省略小數點,四捨五入一樣原理):1998ms(1s)->3000ms(3s)->4002ms(4s)->5004ms(5s)->6006ms(6s)->7006ms(7s)

細究原理

首先,你得先了解js的Event Loop機制裏面的微任務和宏任務,否則,咱們的溝通缺乏了一個平臺。segmentfault

而後,就是問題的關鍵點了,爲何會出現這樣的狀況呢?

答案:以1秒爲例,setInterval和setTimeout都會在異步模塊運行,可是setInterval會每次都剛恰好1s鐘的時候,將微任務隊列的函數,交於任務隊列進行執行。若是setInterval第一次1.004s的時候將任務推動隊列,那麼這時候setInterval的每次的偏差會是在0.996s-1.01s內波動,而setTimeout每次的偏差值都會大於1s。這裏我畫個圖,相信你們就可以明白setInterval實現倒計時的很差之處(圖中進制以省略小數點爲準,四捨五入同理)異步

image

附上一個react hooks版本的倒計時組件給你們

// 倒計時組件
import React, { useState, useEffect } from 'react';

interface CountdownInfo {
  hour: number;
  minute: number;
  second: number;
  day: number;
}

interface CountdownProps {
  sec: number;
  render: (data: CountdownInfo) => React.ReactNode;
  onEnd: () => void;
}

const Countdown: React.FC<CountdownProps> = ({ sec, render, onEnd }) => {
  const [endTime, setEndTime] = useState(new Date(Date.now() + sec*1000));
  const [countdownInfo, setCountdownInfo] = useState({
    hour: 0,
    minute: 0,
    second: 0,
    day: 0,
  } as CountdownInfo);

  const tick = () => {
    const seconds = Math.floor((endTime.getTime() - Date.now())/1000);
    const day = Math.floor(seconds / 86400);
    const hour = Math.floor((seconds % 86400) / 3600);
    const minute = Math.floor((seconds % 3600) / 60);
    const sec = seconds % 60;
    setCountdownInfo({
      day,
      hour,
      minute,
      second: sec,
    } as CountdownInfo);

    if(seconds <= 0) {
      onEnd && onEnd();
    } else {
      window.setTimeout(tick, 1000);    // 時間不截至一直倒計時
    }
  }

  useEffect(() => {
    setEndTime(new Date(Date.now() + sec*1000));
  }, [sec]);
  useEffect(() => {
    tick(); // 初始化倒計時
  }, []);
  return render(countdownInfo) || null;
}

export default Countdown;

引用方式函數

const renderCountdown = ({day, hour, minute, second}): React.ReactNode => {
    return (
      <div>
        <span>{ day > 0 ? (day+'天') : null }</span>
        <span>hour</span>:
        <span>minute</span>:
        <span>second</span>
      </div>
    );
}

const onEnd = () => {
    console.log("倒計時結束")
}

<Countdown 
    sec={100} 
    render={renderCountdown} 
    onEnd={onEnd}
/>
相關文章
相關標籤/搜索