對于一個編寫基礎代碼的程序員來說,理解Javascript定時器的工作原理是很重要的。由于Javascript的定時器工作在一個單線程的環 境中,因此它們常常表現出一些違反直覺的行為。下面我們就首先從三個被用來創建和操作定時器函數入手來分析定時器的工作原理。
- var id = setTimeout(fn, delay); 該函數初始化一個延遲為 delay 的定時器并返回該定時器的id,在定時器觸發前,我們可以通過返回的定時器id來取消這個定時器。當該定時器觸發時將調用 fn 這個函數。
- var id = setInterval(fn, delay); 該函數和 setTimeout 類似,但它會每隔 delay 的時間間隔調用 fn 函數直到該定時器被取消。
- clearInterval(id);, clearTimeout(id); 這兩個函數都接受一個定時器id(前面兩個函數的返回值)作為參數,用來取消相應的定時器。
為了搞清楚定時器的內部是如何工作的,我們需要證實這樣一個事實:定時器的延遲時間是不能夠被保證的。這是因為所有在瀏覽器環境中的 Javascript 都是在一個 單線程中執行的,只有在遇到兩個“執行窗口”的縫隙的時候那些異步的事件(用戶點擊鼠標、定時器觸發)才能被執行。下面這個圖例很好的演示了這一點:
上面的圖中包含了很多需要理解的信息,一旦完全理解了這些,你會對 Javascript 的異步時間的執行有更加清晰的認識。在上面的這個一維的圖示中,豎直方向是以微妙 為單位的時間,藍色框代表 Javascript 執行的代碼塊。例如,上圖中第一個代碼塊執行時間約為18毫秒,鼠標點擊(Mouse Click)事件的回調函數執行了大約11毫秒。
因為 Javascript 在同一時間只能執行某個代碼塊(這是由它單線程的本質決定的),當這個代碼塊執行的時候,異步事件的響應就被“阻塞”了,這意味著這時候產生 的異步事件(鼠標點擊、定時器觸發、XMLHttpRequest請求完成)被加入到一個隊列中(不同的瀏覽器處理事件緩存的方式有很大的差異,這里我們 只需要認為事件被放入了 一個隊列就可以了)等待著下次機會執行。
在上圖中,在第一個代碼塊執行期間初始化了兩個定時器:一個10毫秒的 setTimeout 和一個10毫秒的 setInterval。由于在定時器觸發的時候,第一個代碼塊還沒有執行完成, 因此定時器設定的回調函數不會被立即執行,相反它會被加入隊列等待下次機會執行。
另外,在第一個代碼塊執行過程中發生了一次鼠標點擊事件,與這個異步事件(因為我們不能確定鼠標點擊事件什么時候會發生,因此也認為它是異步的)相 關聯的回調函數也 不會立即執行,它同樣被加入隊列等待下次機會執行。
當第一個代碼塊執行完,瀏覽器會查詢是否有等待執行的任務?而當前情況下,鼠標點擊事件和定時器的回調函數都等待執行,于是瀏覽器按照順序先取出鼠 標點擊事件的回調函數并立即執行它,而定時器的回調函數仍需要等待下次機會執行。
我們注意到,在鼠標點擊事件的回調函數執行過程中,“間隔定時器”(interval)被觸發,和普通定時器一樣,它的回調函數也被加入隊列等待機 會執行。但是,當“間隔定時器”再次被觸發(在普通定時器的回調函數的執行期間)的時候,它的回調函數被丟棄而不是被加入等待隊列。假設所有的“間隔定時 器”的回調函數無論如何都被加入等待隊列的話,那么在執行一個非常大的代碼塊的時候就會有大批的回調函數被加入等待隊列,等到代碼塊執行結束,這一批回調 函數就會無間隔的執行。與此相反,瀏覽器更傾向于在把“間隔定時器”的回調函數加入等待隊列之前簡單的等待直到等待隊列中沒有其他的“間隔定時器”的回調 函數。
實際上我們可以看到,這正是“間隔定時器”的回調函數正在執行的時候另一個個“間隔定時器”被觸發的情形。這向我們揭示了一個重要的事實:“間隔定 時器”不關心當前正在運行的回調函數是什么,只是簡單的把回調函數加入等待隊列即使這意味著這兩個回調函數將會無間隔的被執行。
最后,當第二個“間隔定時器”的回調函數執行結束,我們看到已經沒有等待處理的回調函數了,這時候瀏覽器等待新的異步事件發生。當到達 50ms 標記的時候,“間隔定時器”再次被觸發,這時候已經沒有等待執行的任務,因此回調函數立即被執行。
下面讓我們看一個更加能夠說明 setTimeout 和 setInterval 之間區別的例子:
setTimeout(function(){
/* Some long block of code... */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* Some long block of code... */
}, 10);
當第一眼看上去的時候也許你會覺得這兩個函數功能是完全一樣的,但實際上它們并不相同。使用 setTimeout 的函數會保證它們執行的間隔至少為 10 毫秒(只可能多不可能少),而使用 setInterval 的代碼會嘗試每隔 10 毫秒執行一次,它不會在乎上一次執行到現在的間隔有多少。
在上面我們學到了很多東西,讓我們總結一下:
- Javascript 引擎是單線程的,致使異步事件的回調函數會被加入隊列等待機會執行。
- setTimeout 和 setInterval 在如何執行異步代碼上有根本的區別
- 如果定時器(的回調函數)不能夠被立即執行,那么它將被推遲到下次機會執行(這將大于它預期的延遲)
- 如果回調函數執行時間過長(長于定時器的延遲時間),“間隔定時器”有可能會一個接一個無間隔的執行
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.