爲何要用 setTimeout 模擬 setInterval ?

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

用多了你就會發現它並非準確無誤,極端狀況下還會出現一些使人費解的問題。ajax

下面咱們一一羅列..服務器

推入任務隊列後的時間不許確

定時器代碼:網絡

setInterval(fn(), N);

上面這句代碼的意思實際上是fn()將會在 N 秒以後被推入任務隊列函數

因此,在 setInterval 被推入任務隊列時,若是在它前面有不少任務或者某個任務等待時間較長好比網絡請求等,那麼這個定時器的執行時間和咱們預約它執行的時間可能並不一致。spa

好比:.net

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 操做。這樣一來,花的時間就比較長,有可能前一次代碼尚未執行完,後一次代碼就被添加到隊列了。也會到時定時器變得不許確,甚至出現同一時間執行兩次的狀況。code

最多見的出現的就是,當咱們須要使用 ajax 輪詢服務器是否有新數據時,一定會有一些人會使用 setInterval,然而不管網絡情況如何,它都會去一遍又一遍的發送請求,最後的間隔時間可能和原定的時間有很大的出入。orm

// 作一個網絡輪詢,每一秒查詢一次數據。
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事件推到消息隊列中

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 缺點是很明顯的,爲了解決這些弊端,可使用 settTimeout() 代替。

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

具體實現以下:

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

參考

相關文章
相關標籤/搜索