在JS 事件循環(huán)之宏任務(wù)和微任務(wù)中講到過,setInterval 是一個宏任務(wù)。
用多了你就會發(fā)現(xiàn)它并不是準(zhǔn)確無誤,極端情況下還會出現(xiàn)一些令人費解的問題。
下面我們一一羅列..
推入任務(wù)隊列后的時間不準(zhǔn)確
定時器代碼:
setInterval(fn(), N);
上面這句代碼的意思其實是fn()將會在 N 秒之后被推入任務(wù)隊列氛改。
所以,在 setInterval 被推入任務(wù)隊列時颜阐,如果在它前面有很多任務(wù)或者某個任務(wù)等待時間較長比如網(wǎng)絡(luò)請求等平窘,那么這個定時器的執(zhí)行時間和我們預(yù)定它執(zhí)行的時間可能并不一致。
比如:
let startTime = new Date().getTime();
let count = 0;
//耗時任務(wù)
setInterval(function() {
let i = 0;
while (i++ < 1000000000);
}, 0);
setInterval(function() {
count++;
console.log(
"與原設(shè)定的間隔時差了:",
new Date().getTime() - (startTime + count * 1000),
"毫秒"
);
}, 1000);
// 輸出:
// 與原設(shè)定的間隔時差了: 699 毫秒
// 與原設(shè)定的間隔時差了: 771 毫秒
// 與原設(shè)定的間隔時差了: 887 毫秒
// 與原設(shè)定的間隔時差了: 981 毫秒
// 與原設(shè)定的間隔時差了: 1142 毫秒
// 與原設(shè)定的間隔時差了: 1822 毫秒
// 與原設(shè)定的間隔時差了: 1891 毫秒
// 與原設(shè)定的間隔時差了: 2001 毫秒
// 與原設(shè)定的間隔時差了: 2748 毫秒
// ...
可以看出來凳怨,相差的時間是越來越大的瑰艘,越來越不準(zhǔn)確。
函數(shù)操作耗時過長導(dǎo)致的不準(zhǔn)確
考慮極端情況肤舞,假如定時器里面的代碼需要進(jìn)行大量的計算(耗費時間較長)紫新,或者是 DOM 操作。這樣一來李剖,花的時間就比較長芒率,有可能前一次代碼還沒有執(zhí)行完,后一次代碼就被添加到隊列了篙顺。也會到時定時器變得不準(zhǔn)確偶芍,甚至出現(xiàn)同一時間執(zhí)行兩次的情況。
最常見的出現(xiàn)的就是德玫,當(dāng)我們需要使用 ajax 輪詢服務(wù)器是否有新數(shù)據(jù)時匪蟀,必定會有一些人會使用 setInterval,然而無論網(wǎng)絡(luò)狀況如何宰僧,它都會去一遍又一遍的發(fā)送請求材彪,最后的間隔時間可能和原定的時間有很大的出入。
// 做一個網(wǎng)絡(luò)輪詢,每一秒查詢一次數(shù)據(jù)段化。
let startTime = new Date().getTime();
let count = 0;
setInterval(() => {
let i = 0;
while (i++ < 10000000); // 假設(shè)的網(wǎng)絡(luò)延遲
count++;
console.log(
"與原設(shè)定的間隔時差了:",
new Date().getTime() - (startTime + count * 1000),
"毫秒"
);
}, 1000)
輸出:
// 與原設(shè)定的間隔時差了: 567 毫秒
// 與原設(shè)定的間隔時差了: 552 毫秒
// 與原設(shè)定的間隔時差了: 563 毫秒
// 與原設(shè)定的間隔時差了: 554 毫秒(2次)
// 與原設(shè)定的間隔時差了: 564 毫秒
// 與原設(shè)定的間隔時差了: 602 毫秒
// 與原設(shè)定的間隔時差了: 573 毫秒
// 與原設(shè)定的間隔時差了: 633 毫秒
setInterval 缺點 與 setTimeout 的不同
再次強(qiáng)調(diào)嘁捷,定時器指定的時間間隔,表示的是何時將定時器的代碼添加到消息隊列显熏,而不是何時執(zhí)行代碼雄嚣。所以真正何時執(zhí)行代碼的時間是不能保證的,取決于何時被主線程的事件循環(huán)取到喘蟆,并執(zhí)行现诀。
setInterval(function, N)
//即:每隔N秒把function事件推到消息隊列中
上圖可見,setInterval 每隔 100ms 往隊列中添加一個事件履肃;100ms 后,添加 T1 定時器代碼至隊列中坐桩,主線程中還有任務(wù)在執(zhí)行尺棋,所以等待,some event 執(zhí)行結(jié)束后執(zhí)行 T1 定時器代碼绵跷;又過了 100ms膘螟,T2 定時器被添加到隊列中,主線程還在執(zhí)行 T1 代碼碾局,所以等待荆残;又過了 100ms,理論上又要往隊列里推一個定時器代碼净当,但由于此時 T2 還在隊列中内斯,所以 T3 不會被添加(T3 被跳過),結(jié)果就是此時被跳過像啼;這里我們可以看到俘闯,T1 定時器執(zhí)行結(jié)束后馬上執(zhí)行了 T2 代碼,所以并沒有達(dá)到定時器的效果忽冻。
綜上所述真朗,setInterval 有兩個缺點:
- 使用 setInterval 時,某些間隔會被跳過僧诚;
- 可能多個定時器會連續(xù)執(zhí)行遮婶;
可以這么理解:每個 setTimeout 產(chǎn)生的任務(wù)會直接 push 到任務(wù)隊列中;而 setInterval 在每次把任務(wù) push 到任務(wù)隊列前湖笨,都要進(jìn)行一下判斷(看上次的任務(wù)是否仍在隊列中旗扑,如果有則不添加,沒有則添加)赶么。
因而我們一般用 setTimeout 模擬 setInterval肩豁,來規(guī)避掉上面的缺點。
來看一個經(jīng)典的例子來說明他們的不同:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
做過的朋友都知道:是一次輸出了 5 個 5;
那么問題來了:是每隔 1 秒輸出一個 5 ?還是一秒后立即輸出 5 個 5清钥?
答案是:一秒后立即輸出 5 個 5
因為 for 循環(huán)了五次琼锋,所以 setTimeout 被 5 次添加到時間循環(huán)中,等待一秒后全部執(zhí)行祟昭。
為什么是一秒后輸出了 5 個 5 呢缕坎?
簡單來說,因為 for 是主線程代碼篡悟,先執(zhí)行完了谜叹,才輪到執(zhí)行 setTimeout。
當(dāng)然為什么輸出不是 1 到 5搬葬,這個涉及到作用域的問題了荷腊,這里就不解釋了。
那如果換成 setInterval 呢急凰?
for (var i = 0; i < 5; i++) {
setInterval(function() {
console.log(i);
}, 1000);
}
輸出什么女仰?
答案是:每 1 秒輸出 5 個 5。
為什么輸出 5 個 5抡锈?
是因為 setInterval 只在第 for 循環(huán)時被添加了疾忍,后面的并沒有添加,也就是之前說的床三,setInterval 在每次把任務(wù) push 到任務(wù)隊列前一罩,都要進(jìn)行一下判斷(看上次的任務(wù)是否仍在隊列中,如果有則不添加撇簿,沒有則添加)聂渊。
setTimeout 模擬 setInterval
綜上所述,在某些情況下四瘫,setInterval 并不是很準(zhǔn)確的歧沪。為了解決這些弊端,可以使用 settTimeout() 代替莲组。具體實現(xiàn)如下:
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;
}