【JS】856- 為什么用 setTimeout 模擬 setInterval ?

來源:九旬
https://segmentfault.com/a/1190000038829248
在JS 事件循環(huán)之宏任務(wù)和微任務(wù)中講到過,setInterval 是一個(gè)宏任務(wù)。
用多了你就會發(fā)現(xiàn)它并不是準(zhǔn)確無誤,極端情況下還會出現(xiàn)一些令人費(fèi)解的問題。
下面我們一一羅列..
推入任務(wù)隊(duì)列后的時(shí)間不準(zhǔn)確
定時(shí)器代碼:
setInterval(fn ,?N);
上面這句代碼的意思其實(shí)是fn()將會在 N 秒之后被推入任務(wù)隊(duì)列。
所以,在?setInterval?被推入任務(wù)隊(duì)列時(shí),如果在它前面有很多任務(wù)或者某個(gè)任務(wù)等待時(shí)間較長比如網(wǎng)絡(luò)請求等,那么這個(gè)定時(shí)器的執(zhí)行時(shí)間和我們預(yù)定它執(zhí)行的時(shí)間可能并不一致。
比如:
let?startTime?=?new?Date().getTime();
let?count?=?0;
//耗時(shí)任務(wù)
setInterval(function()?{
??let?i?=?0;
??while?(i++?1000000000);
},?0);
setInterval(function()?{
??count++;
??console.log(
????"與原設(shè)定的間隔時(shí)差了:",
????new?Date().getTime()?-?(startTime?+?count?*?1000),
????"毫秒"
??);
},?1000);
//?輸出:
//?與原設(shè)定的間隔時(shí)差了:699 毫秒
//?與原設(shè)定的間隔時(shí)差了:771 毫秒
//?與原設(shè)定的間隔時(shí)差了:887 毫秒
//?與原設(shè)定的間隔時(shí)差了:981 毫秒
//?與原設(shè)定的間隔時(shí)差了:1142 毫秒
//?與原設(shè)定的間隔時(shí)差了:1822 毫秒
//?與原設(shè)定的間隔時(shí)差了:1891 毫秒
//?與原設(shè)定的間隔時(shí)差了:2001 毫秒
//?與原設(shè)定的間隔時(shí)差了:2748 毫秒
//?...
可以看出來,相差的時(shí)間是越來越大的,越來越不準(zhǔn)確。
函數(shù)操作耗時(shí)過長導(dǎo)致的不準(zhǔn)確
考慮極端情況,假如定時(shí)器里面的代碼需要進(jìn)行大量的計(jì)算(耗費(fèi)時(shí)間較長),或者是?DOM?操作。這樣一來,花的時(shí)間就比較長,有可能前一次代碼還沒有執(zhí)行完,后一次代碼就被添加到隊(duì)列了。也會到時(shí)定時(shí)器變得不準(zhǔn)確,甚至出現(xiàn)同一時(shí)間執(zhí)行兩次的情況。
最常見的出現(xiàn)的就是,當(dāng)我們需要使用?ajax?輪詢服務(wù)器是否有新數(shù)據(jù)時(shí),必定會有一些人會使用?setInterval?,然而無論網(wǎng)絡(luò)狀況如何,它都會去一遍又一遍的發(fā)送請求,最后的間隔時(shí)間可能和原定的時(shí)間有很大的出入。
//?做一個(gè)網(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è)定的間隔時(shí)差了:",
????????new?Date().getTime()?-?(startTime?+?count?*?1000),
????????"毫秒"
????);
},?1000)
輸出:
//?與原設(shè)定的間隔時(shí)差了:567 毫秒
//?與原設(shè)定的間隔時(shí)差了:552 毫秒
//?與原設(shè)定的間隔時(shí)差了:563 毫秒
//?與原設(shè)定的間隔時(shí)差了:554 毫秒(2次)
//?與原設(shè)定的間隔時(shí)差了:564 毫秒
//?與原設(shè)定的間隔時(shí)差了:602 毫秒
//?與原設(shè)定的間隔時(shí)差了:573 毫秒
//?與原設(shè)定的間隔時(shí)差了:633 毫秒
setInterval 缺點(diǎn) 與 setTimeout 的不同
再次強(qiáng)調(diào),定時(shí)器指定的時(shí)間間隔,表示的是何時(shí)將定時(shí)器的代碼添加到消息隊(duì)列,而不是何時(shí)執(zhí)行代碼。所以真正何時(shí)執(zhí)行代碼的時(shí)間是不能保證的,取決于何時(shí)被主線程的事件循環(huán)取到,并執(zhí)行。
setInterval(function,?N)
//即:每隔N秒把function事件推到消息隊(duì)列中

上圖可見,setInterval?每隔?100ms?往隊(duì)列中添加一個(gè)事件;100ms?后,添加?T1?定時(shí)器代碼至隊(duì)列中,主線程中還有任務(wù)在執(zhí)行,所以等待,some event?執(zhí)行結(jié)束后執(zhí)行?T1定時(shí)器代碼;又過了?100ms?,?T2?定時(shí)器被添加到隊(duì)列中,主線程還在執(zhí)行?T1?代碼,所以等待;又過了?100ms?,理論上又要往隊(duì)列里推一個(gè)定時(shí)器代碼,但由于此時(shí)?T2?還在隊(duì)列中,所以T3?不會被添加(T3 被跳過),結(jié)果就是此時(shí)被跳過;這里我們可以看到,T1定時(shí)器執(zhí)行結(jié)束后馬上執(zhí)行了?T2?代碼,所以并沒有達(dá)到定時(shí)器的效果。
綜上所述,setInterval?有兩個(gè)缺點(diǎn):
使用? setInterval?時(shí),某些間隔會被跳過;可能多個(gè)定時(shí)器會連續(xù)執(zhí)行;
可以這么理解:每個(gè)?setTimeout?產(chǎn)生的任務(wù)會直接?push?到任務(wù)隊(duì)列中;而?setInterval?在每次把任務(wù)?push?到任務(wù)隊(duì)列前,都要進(jìn)行一下判斷(看上次的任務(wù)是否仍在隊(duì)列中,如果有則不添加,沒有則添加)。
因而我們一般用?setTimeout?模擬?setInterval?,來規(guī)避掉上面的缺點(diǎn)。
來看一個(gè)經(jīng)典的例子來說明他們的不同:
for?(var?i?=?0;?i?5;?i++)?{
??setTimeout(function()?{
????console.log(i);
??},?1000);
}
做過的朋友都知道:是一次輸出了?5?個(gè)?5?; 那么問題來了:是每隔?1?秒輸出一個(gè) 5 ?還是一秒后立即輸出?5?個(gè)?5??答案是:一秒后立即輸出?5?個(gè)?5因?yàn)?for?循環(huán)了五次,所以?setTimeout?被?5?次添加到時(shí)間循環(huán)中,等待一秒后全部執(zhí)行。
為什么是一秒后輸出了?5?個(gè)?5?呢?簡單來說,因?yàn)?for?是主線程代碼,先執(zhí)行完了,才輪到執(zhí)行?setTimeout?。
當(dāng)然為什么輸出不是?1?到?5?,這個(gè)涉及到作用域的問題了,這里就不解釋了。
setTimeout 模擬 setInterval
綜上所述,在某些情況下,setInterval?缺點(diǎn)是很明顯的,為了解決這些弊端,可以使用?setTimeout()?代替。
在前一個(gè)定時(shí)器執(zhí)行完前,不會向隊(duì)列插入新的定時(shí)器(解決缺點(diǎn)一) 保證定時(shí)器間隔(解決缺點(diǎn)二)
具體實(shí)現(xiàn)如下:
1.寫一個(gè)?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.終止定時(shí)器
if?(timer)?{
??window.clearSetTimeout(timer);
??timer?=?null;
}
參考
為什么要用 setTimeout 模擬 setInterval ? 用 setTimeout()代替 setInterval()

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 100+ 篇原創(chuàng)文章
