天天看點

【JS】986- 為什麼要用 setTimeout 模拟 setInterval ?

【JS】986- 為什麼要用 setTimeout 模拟 setInterval ?

在JS 事件循環之宏任務和微任務中講到過,setInterval 是一個宏任務。

用多了你就會發現它并不是準确無誤,極端情況下還會出現一些令人費解的問題。

下面我們一一羅列..

推入任務隊列後的時間不準确

定時器代碼:

setInterval(fn(), N);      

上面這句代碼的意思其實是fn()将會在 N 秒之後被推入任務隊列。

是以,在 ​

​setInterval​

​ 被推入任務隊列時,如果在它前面有很多任務或者某個任務等待時間較長比如網絡請求等,那麼這個定時器的執行時間和我們預定它執行的時間可能并不一緻。

比如:

let startTime = new Date().getTime();
let count = 0;
//耗時任務
setInterval(function() {
  let i = 0;
  while (i++ < 1000000000);
}, 0);
setInterval(function() {
  count++;
  console.log(
    "與原設定的間隔時差了:",
    new Date().getTime() - (startTime + count * 1000),
    "毫秒"
  );
}, 1000);
// 輸出:
// 與原設定的間隔時差了:699 毫秒
// 與原設定的間隔時差了:771 毫秒
// 與原設定的間隔時差了:887 毫秒
// 與原設定的間隔時差了:981 毫秒
// 與原設定的間隔時差了:1142 毫秒
// 與原設定的間隔時差了:1822 毫秒
// 與原設定的間隔時差了:1891 毫秒
// 與原設定的間隔時差了:2001 毫秒
// 與原設定的間隔時差了:2748 毫秒
// ...      

可以看出來,相差的時間是越來越大的,越來越不準确。

函數操作耗時過長導緻的不準确

考慮極端情況,假如定時器裡面的代碼需要進行大量的計算(耗費時間較長),或者是 ​

​DOM​

​ 操作。這樣一來,花的時間就比較長,有可能前一次代碼還沒有執行完,後一次代碼就被添加到隊列了。也會到時定時器變得不準确,甚至出現同一時間執行兩次的情況。

最常見的出現的就是,當我們需要使用 ​

​ajax​

​​ 輪詢伺服器是否有新資料時,必定會有一些人會使用 ​

​setInterval​

​ ,然而無論網絡狀況如何,它都會去一遍又一遍的發送請求,最後的間隔時間可能和原定的時間有很大的出入。

// 做一個網絡輪詢,每一秒查詢一次資料。
let startTime = new Date().getTime();
let count = 0;

setInterval(() {
    let i = 0;
    while (i++ < 10000000); // 假設的網絡延遲
    count++;
    console.log(
        "與原設定的間隔時差了:",
        new Date().getTime() - (startTime + count * 1000),
        "毫秒"
    );
}, 1000)
輸出:
// 與原設定的間隔時差了:567 毫秒
// 與原設定的間隔時差了:552 毫秒
// 與原設定的間隔時差了:563 毫秒
// 與原設定的間隔時差了:554 毫秒(2次)
// 與原設定的間隔時差了:564 毫秒
// 與原設定的間隔時差了:602 毫秒
// 與原設定的間隔時差了:573 毫秒
// 與原設定的間隔時差了:633 毫秒      

setInterval 缺點 與 setTimeout 的不同

再次強調,定時器指定的時間間隔,表示的是何時将定時器的代碼添加到消息隊列,而不是何時執行代碼。是以真正何時執行代碼的時間是不能保證的,取決于何時被主線程的事件循環取到,并執行。
setInterval(function, N)
//即:每隔N秒把function事件推到消息隊列中      
【JS】986- 為什麼要用 setTimeout 模拟 setInterval ?

setinterval-1.png

上圖可見,​

​setInterval​

​​ 每隔 ​

​100ms​

​​ 往隊列中添加一個事件;​

​100ms​

​​ 後,添加 ​

​T1​

​​ 定時器代碼至隊列中,主線程中還有任務在執行,是以等待,​

​some event​

​​ 執行結束後執行 ​

​T1​

​​ 定時器代碼;又過了 ​

​100ms​

​​ , ​

​T2​

​​ 定時器被添加到隊列中,主線程還在執行 ​

​T1​

​​ 代碼,是以等待;又過了 ​

​100ms​

​ ,理論上又要往隊列裡推一個定時器代碼,但由于此時 ​

​T2​

​ 還在隊列中,是以​

​T3​

​ 不會被添加(T3 被跳過),結果就是此時被跳過;這裡我們可以看到,​

​T1​

​​ 定時器執行結束後馬上執行了 ​

​T2​

​ 代碼,是以并沒有達到定時器的效果。

綜上所述,​

​setInterval​

​ 有兩個缺點:

  • 使用​

    ​setInterval​

    ​ 時,某些間隔會被跳過;
  • 可能多個定時器會連續執行;

可以這麼了解:每個 ​

​setTimeout​

​ 産生的任務會直接 ​

​push​

​ 到任務隊列中;而 ​

​setInterval​

​ 在每次把任務 ​

​push​

​ 到任務隊列前,都要進行一下判斷(看上次的任務是否仍在隊列中,如果有則不添加,沒有則添加)。

因而我們一般用 ​

​setTimeout​

​​ 模拟 ​

​setInterval​

​ ,來規避掉上面的缺點。

來看一個經典的例子來說明他們的不同:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}      

做過的朋友都知道:是一次輸出了 ​

​5​

​​ 個 ​

​5​

​​ ;

那麼問題來了:是每隔 ​​

​1​

​​ 秒輸出一個 5 ?還是一秒後立即輸出 ​

​5​

​​ 個 ​

​5​

​​ ?答案是:一秒後立即輸出 ​

​5​

​​ 個 ​

​5​

​​因為 ​

​for​

​​ 循環了五次,是以 ​

​setTimeout​

​​ 被 ​

​5​

​ 次添加到時間循環中,等待一秒後全部執行。

為什麼是一秒後輸出了 ​

​5​

​ 個 ​

​5​

​ 呢?簡單來說,因為 ​

​for​

​​ 是主線程代碼,先執行完了,才輪到執行 ​

​setTimeout​

​ 。

當然為什麼輸出不是 ​

​1​

​​ 到 ​

​5​

​ ,這個涉及到作用域的問題了,這裡就不解釋了。

setTimeout 模拟 setInterval

綜上所述,在某些情況下,​

​setInterval​

​​ 缺點是很明顯的,為了解決這些弊端,可以使用 ​

​setTimeout()​

​ 代替。

  • 在前一個定時器執行完前,不會向隊列插入新的定時器(解決缺點一)
  • 保證定時器間隔(解決缺點二)

具體實作如下:

1.寫一個 ​

​interval​

​ 方法

let timer = null
interval(func, wait){
    let interv = function(){
        func.call(null);
        timer=setTimeout(interv, wait);
    };
    timer= setTimeout(interv, wait);
 },      

2.和 ​

​setInterval()​

​ 一樣使用它

interval(function() {}, 20);      

3.終止定時器

if (timer) {
  window.clearSetTimeout(timer);
  timer = null;
}      

參考

  • 為什麼要用 setTimeout 模拟 setInterval ?
  • 用 settTimeout()代替 setInterval()