看下面內(nèi)容之前,看一小段代碼,如果讀者能說(shuō)出代碼的用意,那就沒(méi)必要往下看了,因?yàn)槟愣级?/p>
setTimeout(function(){
/* Some long block of code… */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* Some long block of code… */
}, 10);
計(jì)時(shí)器是一個(gè)很牛X的東西,但是很多人其實(shí)只限于知道它的語(yǔ)法,缺乏對(duì)其原理的認(rèn)識(shí)。計(jì)時(shí)器通過(guò)設(shè)定一定的時(shí)間段(毫秒)來(lái)異步的執(zhí)行一段代碼。因?yàn)?Javascript 是一個(gè)單線程語(yǔ)言,計(jì)時(shí)器提供了一種繞過(guò)這種語(yǔ)言限制來(lái)執(zhí)行代碼的能力。
今天就簡(jiǎn)單的來(lái)說(shuō)下計(jì)時(shí)器的工作原理。
JavaScript 提供了三個(gè)函數(shù)來(lái)構(gòu)建和操作計(jì)時(shí)器
var id = setTimeout(fn, delay);var id = setInterval(fn, delay);clearInterval(id); clearTimeout(id);
具體的語(yǔ)法我就不多說(shuō)了,可以查手冊(cè)。為了了解計(jì)時(shí)器的工作原理,有一個(gè)概念必須記在心里:時(shí)間延遲不能被保證。什么意思,就是說(shuō)你這樣寫(xiě)setTimeout(fn, 500)并不代表fn肯定在500毫秒之后馬上就執(zhí)行,延遲很可能會(huì)更長(zhǎng)。因?yàn)?JavaScript 是單線程語(yǔ)言,所有的異步事件(包括計(jì)時(shí)器、鼠標(biāo)事件或者一個(gè) XMLHttpRequest 完成)僅僅當(dāng)程序執(zhí)行期間有缺口的時(shí)候才會(huì)執(zhí)行,不是你規(guī)定了什么時(shí)候就什么時(shí)候執(zhí)行,要知道程序員不是萬(wàn)能的,你寫(xiě)的東西最終還是要看瀏覽器臉色的。
下面的這張圖片可以很好的說(shuō)明問(wèn)題,感謝 John Resig 大神。

從上往下看,左面的數(shù)字代表時(shí)間(毫秒),右面的文字代表了一系列異步事件的設(shè)置和觸發(fā),中間則是代碼塊。最上面的 JavaScript 代碼塊可能是你在瀏覽器載入的時(shí)候執(zhí)行的片段,大概耗時(shí)18毫秒,緊接著下面的 Mouse Click Callback 代碼塊可能是你一個(gè)鼠標(biāo)事件觸發(fā)時(shí)的回調(diào)函數(shù),大概耗時(shí)11毫秒,依次類(lèi)推。
JavaScript 的單線程特性決定了每次只能執(zhí)行一塊,所以當(dāng)?shù)谝粔K代碼執(zhí)行的時(shí)候(它一共運(yùn)行了18毫秒),本身構(gòu)造了兩個(gè)計(jì)時(shí)器,期間可能用戶還點(diǎn)了一下鼠標(biāo)(你有過(guò)在網(wǎng)頁(yè)一打開(kāi)還沒(méi)載完就在那亂點(diǎn)的情況嗎)。按理說(shuō)用戶點(diǎn)完鼠標(biāo)就應(yīng)該馬上執(zhí)行那個(gè)回調(diào)函數(shù),但是不行,JavaScript 執(zhí)行只有一條道嘛,在那18毫秒沒(méi)跑完之前,其他代碼塊想執(zhí)行就只能排隊(duì),沒(méi)空間給你超車(chē)都。那兩個(gè)計(jì)時(shí)器都是10毫秒的延遲,從圖中可以看到,setTimeout也在那18毫秒執(zhí)行結(jié)束之前觸發(fā)了,沒(méi)辦法也排隊(duì)吧。
終于,18毫秒后,天上一道神雷把前面的車(chē)直接劈成空氣了,后面兩個(gè)排隊(duì)的可以過(guò)去了,但是還得一個(gè)一個(gè),不能并列,那誰(shuí)先過(guò)去呢?是不是兩個(gè)人在那劃拳?不是的,瀏覽器說(shuō)的算,瀏覽器說(shuō),鼠標(biāo)單擊事件先過(guò)去,setTimeout只能繼續(xù)等11毫秒。注意看圖,在鼠標(biāo)事件回調(diào)函數(shù)執(zhí)行的時(shí)候,又一個(gè)計(jì)時(shí)器事件觸發(fā)了(setInterval),等著,而且必須排在setTimeout的后面。
11毫秒過(guò)去了,setTimeout 終于可以過(guò)去了,注意看,setInterval 的第二次觸發(fā)了,雖然它第一次都在排隊(duì)呢,如果這個(gè)時(shí)候還向往常一樣排隊(duì),最后是什么情況,setTimeout執(zhí)行完了,就會(huì)連續(xù)執(zhí)行兩個(gè)setInterval,你設(shè)置的延遲沒(méi)用了都。所以瀏覽器還是比較智能的,它在處理setInterval的時(shí)候,如果發(fā)現(xiàn)已經(jīng)有排隊(duì)的,就直接把新來(lái)的 Kill 掉。
接著看,輪到排隊(duì)的 setInterval 第一次觸發(fā)開(kāi)始執(zhí)行了,它執(zhí)行的時(shí)候,第三次觸發(fā)又到了,這一次沒(méi)有排隊(duì)了,所以瀏覽器沒(méi)把它 Kill 掉,給丫排隊(duì)的機(jī)會(huì),所以你會(huì)發(fā)現(xiàn)這兩次的setInterval的執(zhí)行沒(méi)有間隔的,如果你做一個(gè)幻燈片,遇到這種情況就要好好想想自己的代碼是不是有問(wèn)題了。
最后,再也沒(méi)有別的因素干擾了 setInterval 了(假如用戶被 MM 叫走了),setInterval 就按照你想要的步驟執(zhí)行了。
講到這里,開(kāi)頭的代碼可以理解了吧。
setTimeout(function(){
/* Some long block of code… */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* Some long block of code… */
}, 10);
這兩個(gè)函數(shù)看起來(lái)效果一樣,其實(shí)不然,第一個(gè)代碼塊總會(huì)延遲10毫秒執(zhí)行,雖然大多時(shí)候是大于10毫秒的。而第二個(gè)每到10毫秒就嘗試執(zhí)行,不管之前的觸發(fā)執(zhí)行了沒(méi)有。
總結(jié)起來(lái)四條:
- JavaScript 引擎只有一個(gè)線程,它會(huì)迫使某些異步事件排隊(duì)
- setTimeout 和 setInterval 在執(zhí)行異步代碼的時(shí)候有很大區(qū)別
- 假如一個(gè)計(jì)時(shí)器被阻止執(zhí)行,它會(huì)等待知道遇到一個(gè)代碼執(zhí)行空隙,通常時(shí)間比預(yù)計(jì)的要長(zhǎng)
- Intervals 可能會(huì)一個(gè)挨著一個(gè)執(zhí)行,如果回調(diào)函數(shù)的執(zhí)行時(shí)間大于間隔
翻譯鏈接:http://www.daqianduan.com/1112.html
原文鏈接:https://johnresig.com/blog/how-javascript-timers-work/