如果你了解JS 事件循環之宏任務和微任務的話,那么你就很清楚 setInterval 是一個宏任務。用多了你就會發現它並不是准確無誤,極端情況下還會出現一些令人費解的問題。下面我們一一羅列:
一、setInterval()常見問題
1、推入任務隊列后的時間不准確
定時器代碼:setInterval(fn, num),這句代碼的意思其實是fn()將會在 num 秒之后被推入任務隊列。
所以,在 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 毫秒 // ...
可以看出來,相差的時間是越來越大的,越來越不准確。
2、函數操作耗時過長導致的不准確
考慮極端情況,假如定時器里面的代碼需要進行大量的計算(耗費時間較長),或者是 DOM
操作。這樣一來,花的時間就比較長,有可能前一次代碼還沒有執行完,后一次代碼就被添加到隊列了。也會到時定時器變得不准確,甚至出現同一時間執行兩次的情況。
最常見的出現的就是,當我們需要使用 ajax
輪詢服務器是否有新數據時,必定會有一些人會使用 setInterval
,然而無論網絡狀況如何,它都會去一遍又一遍的發送請求,最后的間隔時間可能和原定的時間有很大的出入。
二、setInterval 缺點 與 setTimeout 的不同
再次強調,定時器指定的時間間隔,表示的是何時將定時器的代碼添加到消息隊列,而不是何時執行代碼。所以真正何時執行代碼的時間是不能保證的,取決於何時被主線程的事件循環取到,並執行。
setInterval(function, N) //即:每隔N秒把function事件推到消息隊列中
上圖可見,setInterval
每隔 100ms
往隊列中添加一個事件;100ms
后,添加 T1
定時器代碼至隊列中,主線程中還有任務在執行,所以等待,some event
執行結束后執行 T1
定時器代碼;又過了 100ms
, T2
定時器被添加到隊列中,主線程還在執行 T1
代碼,所以等待;又過了100ms
,理論上又要往隊列里推一個定時器代碼,但由於此時 T2
還在隊列中,所以T3
不會被添加(T3 被跳過),結果就是此時被跳過;這里我們可以看到,T1
定時器執行結束后馬上執行了 T2
代碼,所以並沒有達到定時器的效果。
綜上所述,setInterval
有兩個缺點:
(1)使用 setInterval
時,某些間隔會被跳過;
(2)可能多個定時器會連續執行;
可以這么理解:每個 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)在前一個定時器執行完前,不會向隊列插入新的定時器(解決缺點一)
(2)保證定時器間隔(解決缺點二)
具體實現如下:
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; }
來源:九旬
https://segmentfault.com/a/1190000038829248