對于一個編寫基礎(chǔ)代碼的程序員來說,理解Javascript定時器的工作原理是很重要的。由于Javascript的定時器工作在一個單線程的環(huán) 境中,因此它們常常表現(xiàn)出一些違反直覺的行為。下面我們就首先從三個被用來創(chuàng)建和操作定時器函數(shù)入手來分析定時器的工作原理。
- var id = setTimeout(fn, delay); 該函數(shù)初始化一個延遲為 delay 的定時器并返回該定時器的id,在定時器觸發(fā)前,我們可以通過返回的定時器id來取消這個定時器。當(dāng)該定時器觸發(fā)時將調(diào)用 fn 這個函數(shù)。
- var id = setInterval(fn, delay); 該函數(shù)和 setTimeout 類似,但它會每隔 delay 的時間間隔調(diào)用 fn 函數(shù)直到該定時器被取消。
- clearInterval(id);, clearTimeout(id); 這兩個函數(shù)都接受一個定時器id(前面兩個函數(shù)的返回值)作為參數(shù),用來取消相應(yīng)的定時器。
為了搞清楚定時器的內(nèi)部是如何工作的,我們需要證實這樣一個事實:定時器的延遲時間是不能夠被保證的。這是因為所有在瀏覽器環(huán)境中的 Javascript 都是在一個 單線程中執(zhí)行的,只有在遇到兩個“執(zhí)行窗口”的縫隙的時候那些異步的事件(用戶點擊鼠標(biāo)、定時器觸發(fā))才能被執(zhí)行。下面這個圖例很好的演示了這一點:
上面的圖中包含了很多需要理解的信息,一旦完全理解了這些,你會對 Javascript 的異步時間的執(zhí)行有更加清晰的認(rèn)識。在上面的這個一維的圖示中,豎直方向是以微妙 為單位的時間,藍(lán)色框代表 Javascript 執(zhí)行的代碼塊。例如,上圖中第一個代碼塊執(zhí)行時間約為18毫秒,鼠標(biāo)點擊(Mouse Click)事件的回調(diào)函數(shù)執(zhí)行了大約11毫秒。
因為 Javascript 在同一時間只能執(zhí)行某個代碼塊(這是由它單線程的本質(zhì)決定的),當(dāng)這個代碼塊執(zhí)行的時候,異步事件的響應(yīng)就被“阻塞”了,這意味著這時候產(chǎn)生 的異步事件(鼠標(biāo)點擊、定時器觸發(fā)、XMLHttpRequest請求完成)被加入到一個隊列中(不同的瀏覽器處理事件緩存的方式有很大的差異,這里我們 只需要認(rèn)為事件被放入了 一個隊列就可以了)等待著下次機會執(zhí)行。
在上圖中,在第一個代碼塊執(zhí)行期間初始化了兩個定時器:一個10毫秒的 setTimeout 和一個10毫秒的 setInterval。由于在定時器觸發(fā)的時候,第一個代碼塊還沒有執(zhí)行完成, 因此定時器設(shè)定的回調(diào)函數(shù)不會被立即執(zhí)行,相反它會被加入隊列等待下次機會執(zhí)行。
另外,在第一個代碼塊執(zhí)行過程中發(fā)生了一次鼠標(biāo)點擊事件,與這個異步事件(因為我們不能確定鼠標(biāo)點擊事件什么時候會發(fā)生,因此也認(rèn)為它是異步的)相 關(guān)聯(lián)的回調(diào)函數(shù)也 不會立即執(zhí)行,它同樣被加入隊列等待下次機會執(zhí)行。
當(dāng)?shù)谝粋€代碼塊執(zhí)行完,瀏覽器會查詢是否有等待執(zhí)行的任務(wù)?而當(dāng)前情況下,鼠標(biāo)點擊事件和定時器的回調(diào)函數(shù)都等待執(zhí)行,于是瀏覽器按照順序先取出鼠 標(biāo)點擊事件的回調(diào)函數(shù)并立即執(zhí)行它,而定時器的回調(diào)函數(shù)仍需要等待下次機會執(zhí)行。
我們注意到,在鼠標(biāo)點擊事件的回調(diào)函數(shù)執(zhí)行過程中,“間隔定時器”(interval)被觸發(fā),和普通定時器一樣,它的回調(diào)函數(shù)也被加入隊列等待機 會執(zhí)行。但是,當(dāng)“間隔定時器”再次被觸發(fā)(在普通定時器的回調(diào)函數(shù)的執(zhí)行期間)的時候,它的回調(diào)函數(shù)被丟棄而不是被加入等待隊列。假設(shè)所有的“間隔定時 器”的回調(diào)函數(shù)無論如何都被加入等待隊列的話,那么在執(zhí)行一個非常大的代碼塊的時候就會有大批的回調(diào)函數(shù)被加入等待隊列,等到代碼塊執(zhí)行結(jié)束,這一批回調(diào) 函數(shù)就會無間隔的執(zhí)行。與此相反,瀏覽器更傾向于在把“間隔定時器”的回調(diào)函數(shù)加入等待隊列之前簡單的等待直到等待隊列中沒有其他的“間隔定時器”的回調(diào) 函數(shù)。
實際上我們可以看到,這正是“間隔定時器”的回調(diào)函數(shù)正在執(zhí)行的時候另一個個“間隔定時器”被觸發(fā)的情形。這向我們揭示了一個重要的事實:“間隔定 時器”不關(guān)心當(dāng)前正在運行的回調(diào)函數(shù)是什么,只是簡單的把回調(diào)函數(shù)加入等待隊列即使這意味著這兩個回調(diào)函數(shù)將會無間隔的被執(zhí)行。
最后,當(dāng)?shù)诙€“間隔定時器”的回調(diào)函數(shù)執(zhí)行結(jié)束,我們看到已經(jīng)沒有等待處理的回調(diào)函數(shù)了,這時候瀏覽器等待新的異步事件發(fā)生。當(dāng)?shù)竭_(dá) 50ms 標(biāo)記的時候,“間隔定時器”再次被觸發(fā),這時候已經(jīng)沒有等待執(zhí)行的任務(wù),因此回調(diào)函數(shù)立即被執(zhí)行。
下面讓我們看一個更加能夠說明 setTimeout 和 setInterval 之間區(qū)別的例子:
setTimeout(function(){
/* Some long block of code... */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* Some long block of code... */
}, 10);
當(dāng)?shù)谝谎劭瓷先サ臅r候也許你會覺得這兩個函數(shù)功能是完全一樣的,但實際上它們并不相同。使用 setTimeout 的函數(shù)會保證它們執(zhí)行的間隔至少為 10 毫秒(只可能多不可能少),而使用 setInterval 的代碼會嘗試每隔 10 毫秒執(zhí)行一次,它不會在乎上一次執(zhí)行到現(xiàn)在的間隔有多少。
在上面我們學(xué)到了很多東西,讓我們總結(jié)一下:
- Javascript 引擎是單線程的,致使異步事件的回調(diào)函數(shù)會被加入隊列等待機會執(zhí)行。
- setTimeout 和 setInterval 在如何執(zhí)行異步代碼上有根本的區(qū)別
- 如果定時器(的回調(diào)函數(shù))不能夠被立即執(zhí)行,那么它將被推遲到下次機會執(zhí)行(這將大于它預(yù)期的延遲)
- 如果回調(diào)函數(shù)執(zhí)行時間過長(長于定時器的延遲時間),“間隔定時器”有可能會一個接一個無間隔的執(zhí)行
All of this is incredibly important knowledge to build off of. Knowing how a JavaScript engine works, especially with the large number of asynchronous events that typically occur, makes for a great foundation when building an advanced piece of application code.