沐鳴網址_為什麼要用 setTimeout 模擬 setInterval ?

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事件推到消息隊列中

上圖可見,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;
}

參考

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

來自:https://segmentfault.com/a/1190000038829248

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/10055