JavaScript中的Timer是怎麼工作的

作爲入門者來說,瞭解JavaScript中timer的工作方式是很重要的。通常它們的表現行爲並不是那麼地直觀,而這是因爲它們都處在一個單一線程中。讓我們先來看一看三個用來創建以及操作timer的函數。

  • var id = setTimeout(fn, delay); - 初始化一個單一的timer,這個timer將會在一定延時後去調用指定的函數。這個函數(setTimeout)將返回一個唯一的ID,我們可以通過這個ID來取消timer。
  • var id = setInterval(fn, delay); - 與setTimeout類似,只不過它會持續地調用指定的函數(每次都有一個延時),直到timer被取消爲止。
  • clearInterval(id);clearTimeout(id); - 接受一個timer的ID(由上述的兩個函數返回的),並且停止timer的回調事件。

要搞明白timer在內部是怎麼工作的,我們還需要知道一個很重要的概念:timer的延時並不是每次都能如你所願的。由於在同一個瀏覽器中所有的JavaScript都只在單一線程中執行,那些異步的事件(比如說鼠標點擊,或者timer)只在執行期出現空閒的時候纔會運行。這個用圖最能表示清楚了,請參見下圖:


(點擊查看大圖)

在這個示例中有很多信息可以挖掘,但是完全理解了之後你將會更清楚地認識到異步的JavaScript是怎麼執行的。這是個一維的圖:豎直方向上的是(掛鐘式)時間,單位爲毫秒。藍色的框表示正在執行的JavaScript片段。舉例來說,第一塊JavaScript執行了約18ms,而鼠標點擊則執行了約11ms,以此類推。

由於JavaScript向來都只能在同一時間執行一塊代碼(這是由它單線程的本質決定的),所以每一個代碼塊都“阻塞”了其他的異步事件。這意味着當異步事件發生時(比如鼠標點擊、timer觸發或者是XMLHttpRequest完成),這些事件將進入到一個隊列中等待執行(隊列的實現方法因瀏覽器而異,我們在此只討論一個簡化的情況)。

剛開始,在第一個JavaScript塊中,有兩個timer被初始化了:一個10ms的setTimeout和一個是10ms的setInterval。由於timer(這裏的timer指setTimeout中的timer,而下文中的interval則指setInvertal中的timer)開始的時間,實際上它在第一個代碼塊結束前就已經觸發了。然而請注意,它並不會馬上執行(事實上由於單線程的存在,它也無法做到馬上執行)。相反的,這個被延期執行的函數進入隊列中,等待在空閒的時候被執行。

另外,在第一個JavaScript塊中,我們看到一個鼠標點擊事件也發生了。而與這個異步事件(我們不知道用戶什麼時候會去執行一個動作,因此將其認爲是一個異步動作)相關的JavaScript回調函數也無法立馬執行,正如timer一樣,它也進行到隊列中等待被執行。

當第一個JavaScript塊被執行完之後,瀏覽器問了一個問題:有正在等待被執行的代碼嗎?在這個例子中,鼠標點擊事件和time事件都正在隊列中等待。於是瀏覽器選了一個(鼠標點擊事件),然後馬上執行它。而timer只能繼續等下去。

注意當鼠標點擊事件正在執行的時候第一次的interval事件也觸發了,與timer一樣,它的事件也進入隊列等待之後執行。然而,注意,當interval再次觸發的時候(這個時候timer的事件正在執行),這一次它的事件被丟棄了。如果你在一個大的JavaScript代碼塊正在執行的時候把所有的interval回調函數都囤起來的話,其結果就是在JavaScript代碼塊執行完了之後會有一堆的interval事件被執行,而執行過程中不會有間隔。因此,取代的作法是瀏覽器情願先等一等,以確保在一個interval進入隊列的時候隊列中沒有別的interval。

事實上,我們可以在例子中看出:當第三個interval觸發的時候這個interval自身正在執行。這告訴我們一個重要的事實:interval是不管當前在執行些什麼的,在任何情況下它都會進入到隊列中去,即使這樣意味着每次回調之間的時間就不準確了。

最後,當第二個interval回調執行完後,我們可以看到隊列已經被清空,沒有什麼需要JavaScript引擎去執行的了。這表明瀏覽器現在等待一個新的異步事件發生。於是在50ms的時候我們看到interval又觸發了。這一樣,由於沒有什麼東西擋住了它的執行,它馬上就觸發了。

讓我們來看一個例子,這個例子更好地闡釋了setTimeout和setInveral之間的區別。

  setTimeout(function(){
    /* 一個很長的代碼塊…… */
    setTimeout(arguments.callee10);
  }10);
  
  setInterval(function(){
    /* 一個很長的代碼塊…… */
  }10);

乍看上去,這兩段代碼在功能上似乎是相同的,可實際上並非如此。setTimeout的代碼在前一次的回調執行完後總是至少會有10ms的延時(有可能會更多,但是絕對不會更少);而setInterval則總是在每10ms的時候嘗試執行一次回調,它不管上一次回調是什麼時候執行的。

我們在此學到了很多,讓我們重述一下:

  • JavaScript引擎只有一個線程,這使得異步事件必需列隊等待執行。
  • setTimeout和setInterval在如何執行代碼上有着本質地區別。
  • 如果一個timer在將要執行的時候被阻塞,它將會等待下一個時機(比預期的延時要長)。
  • 如果interval的執行時間較長(比指定的延時長),那麼它們將連續地執行而沒有延時。

以上這些知識是相當重要的。知道JavaScript引擎的工作方式,尤其是知道它在有很多異步事件發生的時候是怎麼工作的,爲我們在寫進階的應用程序代碼打下了堅實的基礎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章