JavaScript中定時器的工作原理(How JavaScript Timers Work)

原文鏈接:https://johnresig.com/blog/how-javascript-timers-work/

JavaScript 定時器工作原理是一個重要的基礎知識點。因為定時器在單執行緒中工作,它們表現出的行為很直觀。

我們該如何創建和維護定時器呢?要從如下三個函數(都是定義在全局作用域,在瀏覽器中就是 Window 的方法)說起:

  • var id=setTimeout(fn,delay); 初始化一個只執行一次的定時器,這個定時器會在指定的時間延遲 delay 之後調用函數 fn ,該 setTimeout 函數返回定時器的唯一 id ,我們可以通過這個 id 來取消定時器的執行。
  • var id=setInvertal(fn,delay);setTimeout 類似,只是它會以 delay 為周期,反覆調用函數 fn ,直到我們通過id取消該定時器。
  • clearInterval(id),clearTimeout(id); 這兩個函數接受定時器的 id(例如我們上面提到的兩個函數產生的定時器 id ),並停止對定時器中指定函數的調用。

要深入理解定時器工作原理,我們需要探索一個重要的概念:定時器指定的延遲時間並不能得到保證。

在瀏覽器中,因為所有的 JavaScript 程式碼都運行在單一執行緒之中,非同步事件(如滑鼠點擊,定時器)只有在他們被觸發的時候他們的回調才有機會得以執行。

我們可以用下圖說明:

圖中包含大量的資訊,吸收並理解這些資訊,能幫助我們領悟「非同步的 JavaScript 程式碼是如何工作的」。

這個圖是一維的,垂直方向是時間,以毫秒為單位。藍色的盒子代表正在執行的javascript程式碼所佔時間片段。

例如,第一個 JavaScript 塊執行時間約 18ms,第二個滑鼠點擊塊執行了約 11ms,其他塊類似。

因為單執行緒的緣故,在同一時間只能執行一條 JavaScript 程式碼,每一個程式碼塊(藍色盒子)都會阻塞其他非同步事件的執行。

這就意味著,當一個非同步事件發生的時候(例如滑鼠點擊,定時器觸發,一個 XMLHttpRequest 請求完成),它進入了程式碼的執行隊列,執行執行緒空閑時會依照該執行隊列中順序依次執行程式碼。(如何將非同步事件加入隊列,不同瀏覽器,他們的實現可能有所差異,所以這裡我們將其簡單化)。

開始的時候,在 JavaScript程式碼塊(第一個盒子),初始化了兩個定時器,一個 10ms 延遲的 setTimeout 和 10ms 的 setInterval 。這些定時器可能會在我們第一個程式碼塊執行結束之前就觸發,這取決於定時器在第一個程式碼塊中啟動的位置和時間。

注意,定時器雖然觸發了,但是並不會立即執行,它只是把需要延遲執行的函數加入了執行隊列,在執行緒的某一個可用的時間點,這個函數就能夠得到執行。

當第一個 JavaScript 程式碼初始化塊執行結束,瀏覽器立即提出一個問題:誰在等待著被執行?

在這個案例中滑鼠點擊時間的處理程式和一個定時器( setTimeout )都在等待。瀏覽器選擇一個並執行(這裡是滑鼠點擊事件的處理程式)。定時器就需要等待下一個可用時間來執行。

需要注意的是當滑鼠點擊事件處理程式執行的時候,第一個 interval 定時器觸發了。和 timeout 定時器一樣,他的回調函數被加入了執行隊列,等待執行。

然而,還需要注意到當 interval 定時器再次觸發,這個時候 timeout 定時器的回調函數正在執行,此時這個 interval 的觸發被放棄了。

假想(瀏覽器不這樣做),在一個佔用時間很多的初始化定時器的程式碼塊中,所有的 interval 觸發都把回調加入執行隊列,當初始化程式碼塊結束後,執行隊列中已經累加了大量的定時器回調函數,結果就會出現大量的 interval 回調函數無間隔的執行,直到該執行隊列清空。所以瀏覽器在講一個 interval 回調加入執行隊列前,會檢查執行隊列,如果其中存在尚未執行的 interval 回調那麼就等待,直到當前執行隊列中沒有相應 interval 的回調以後才會繼續入隊 interval 回調。

事實上,如圖,我們看見在第一個 interval 的回調執行的時候(之前進入執行隊列),第三個 interval 觸發了,這想我們展示一個重要的現象: interval 不關心當前正在執行的程式碼,他們會不加選擇的添加回調到執行隊列,儘管這意味著兩個 interval 回調函數執行的時間間隔被犧牲。這裡第一個 interval 回調執行結束後,緊跟著第三個 interval 的回調馬上得到執行,中間沒有印象中應該有的 10ms 間隔。

最終,在第三個 interval 的回調執行結束後,我們看見執行隊列中沒有等待 JavaScript 引擎執行的程式碼,這就意味著,瀏覽器現在等待新的非同步事件的發生,在 50ms 的刻度處 interval 再次觸發,此時沒有什麼會阻塞 JavaScript 引擎,這個 interval 回調會立即執行。

讓我們看一個例子來闡明,setIntervalsetTimeout 的不同:

setTimeout(function () {    /* Some long block of code... */    setTimeout(arguments.callee, 10);  }, 10);    setInterval(function () {    /* Some long block of code... */  }, 10);

看第一眼,會覺得這兩段程式碼功能相同,實際上,他們是不同的。

需要注意到, setTimeout 的回調函數的執行總是保證了至少 10ms 的間隔(與上一個回調的執行相比,實際執行時,這個間隔可能變長,但是不可能更少),但是 setInterval 會嘗試每隔 10ms 執行一次回調,不管上一個回調函數時候已經執行完畢。(很多類庫的動畫都是使用的 setTimeout 實現)

這裡我們學到很多,總結一下:

  • JavaScript 引擎是單執行緒的,會迫使非同步事件進入執行隊列,等待執行。
  • setTimeout 和 setInterval 在執行非同步程式碼時從根本上是有所不同的。
  • 如果一個定時器事件被阻塞,使得它不能立即執行,那麼它會被延遲,直到下一個可能的時間點,才被執行(這可能比你指定的 delay 時間要長)
  • Interval 的回調有可能『背靠背』無間隔的執行,這種情況是說 interval 的回調函數的執行時間比你指定的 delay 時間還要長

這些都是構建 JavaScript 應用程式非常重要的知識。了解 JavaScript Engine 是如何工作的,特別存在大量的非同步事件發生,為構建高級應用程式程式碼打下基礎。

本文已加入 騰訊雲自媒體分享計劃 (點擊加入)