JavaScript中定時器的工作原理(How JavaScript Timers Work)
- 2020 年 3 月 5 日
- 筆記
原文鏈接: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
回調會立即執行。
讓我們看一個例子來闡明,setInterval
和 setTimeout
的不同:
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 是如何工作的,特別存在大量的非同步事件發生,為構建高級應用程式程式碼打下基礎。
本文已加入 騰訊雲自媒體分享計劃 (點擊加入)