從setTimeout說事件迴圈模型

TAT.ronnie發表於2017-03-28

作為一個從其他程式語言(C#/Java)轉到Javascript的開發人員,在學習Javascript過程中,setTimeout()方法的執行原理是我遇到的一個不太好理解的部分,本文嘗試結合其他程式語言的實現,從setTimeout說事件迴圈模型

1.從setTimeout說起

setTimeout()方法不是ecmascript規範定義的內容,而是屬於BOM提供的功能。檢視w3school對setTimeout()方法的定義,setTimeout() 方法用於在指定的毫秒數後呼叫函式或計算表示式。

語法setTimeout(fn,millisec),其中fn表示要執行的程式碼,可以是一個包含javascript程式碼的字串,也可以是一個函式。第二個引數millisec是以毫秒錶示的時間,表示fn需推遲多長時間執行。

呼叫setTimeout()方法之後,該方法返回一個數字,這個數字是計劃執行程式碼的唯一識別符號,可以通過它來取消超時呼叫。

起初我對 setTimeout()的使用比較簡單,對其執行機理也沒有深入的理解,直到看到下面程式碼

在我最初對setTimeout()的認識中,延時設定為500ms,所以輸出應該為Time elapsed: 500 ms。因為在直觀的理解中,Javascript執行引擎,在執行上述程式碼過程中,應當是一個由上往下的順序執行過程,setTimeout函式是先於while語句執行的。可是實際上,上述程式碼執行多次後,輸出至少是延遲了1000ms。

2.Java對setTimeout的實現

聯想起以往學習Java的經驗,上述Javascript的setTimeout()讓我困惑。Java對setTimeout的實現有多種API實現,這裡我們以java.util.Timer包為例。使用Timer在Java中實現上述邏輯,執行多次,輸出都是Time elapsed: 501 ms。

這裡深究setTimeout()為什麼出現這一差異之前,先說說java.util.Timer的實現原理。

上述程式碼幾個關鍵要素為Timer、TimerTask類以及Timer類的schedule方法,通過閱讀相關原始碼,可以瞭解其實現。

Timer:一個Task任務的排程類,和TimerTask任務一樣,是供使用者使用的API類,通過schedule方法安排Task的執行計劃。該類通過TaskQueue任務佇列和TimerThread類完成Task的排程。

TimerTask:實現Runnable介面,表明每一個任務均為一個獨立的執行緒,通過run()方法提供使用者定製自己任務。

TimerThread:繼承於Thread,是真正執行Task的類。

TaskQueue:儲存Task任務的資料結構,內部由一個最小堆實現,堆的每個成員為TimeTask,每個任務依靠TimerTask的 nextExecutionTime屬性值進行排序,nextExecutionTime最小的任務在佇列的最前端,從而能夠現實最早執行。

3.根據結果找原因

看過了Java.util.Timer對類似setTimeout()的實現方案,繼續回到前文Javascript的setTimeout()方法中,再來看看之前的輸出為什麼與預期不符。

通過閱讀程式碼不難看出,setTimeout()方法執行在while()迴圈之前,它宣告瞭“希望”在500ms之後執行一次匿名函式,這一宣告,也即對匿名函式的註冊,在setTimeout()方法執行後立即生效。程式碼最後一行的while迴圈會持續執行1000ms,通過setTimeout()方法註冊的匿名函式輸出的延遲時間總是大於1000ms,說明對這一匿名函式的實際呼叫被while()迴圈阻塞了,實際的呼叫在while()迴圈阻塞結束後才真正執行。

而在Java.util.Timer中,對於定時任務的解決方案是通過多執行緒手段實現的,任務物件儲存在任務佇列,由專門的排程執行緒,在新的子執行緒中完成任務的執行。通過schedule()方法註冊一個非同步任務時,排程執行緒在子執行緒立即開始工作,主執行緒不會阻塞任務的執行。

這就是Javascript與Java/C#之類語言的一大差異,即Javascript的單執行緒機制。在現有瀏覽器環境中,Javascript執行引擎是單執行緒的,主執行緒的語句和方法,會阻塞定時任務的執行,執行引擎只有在執行完主執行緒的語句後,定時任務才會實際執行,這期間的時間,可能大於註冊任務時設定的延時時間。在這一點上,Javascript與Java/C#的機制很不同。

4.事件迴圈模型

在單執行緒的Javascript引擎中,setTimeout()是如何執行的呢,這裡就要提到瀏覽器核心中的事件迴圈模型了。簡單的講,在Javascript執行引擎之外,有一個任務佇列,當在程式碼中呼叫setTimeout()方法時,註冊的延時方法會交由瀏覽器核心其他模組(以webkit為例,是webcore模組)處理,當延時方法到達觸發條件,即到達設定的延時時間時,這一延時方法被新增至任務佇列裡。這一過程由瀏覽器核心其他模組處理,與執行引擎主執行緒獨立,執行引擎在主執行緒方法執行完畢,到達空閒狀態時,會從任務佇列中順序獲取任務來執行,這一過程是一個不斷迴圈的過程,稱為事件迴圈模型。

參考一個演講中的資料,上述事件迴圈模型可以用下圖描述。

Javascript執行引擎的主執行緒執行的時候,產生堆(heap)和棧(stack)。程式中程式碼依次進入棧中等待執行,當呼叫setTimeout()方法時,即圖中右側WebAPIs方法時,瀏覽器核心相應模組開始延時方法的處理,當延時方法到達觸發條件時,方法被新增到用於回撥的任務佇列,只要執行引擎棧中的程式碼執行完畢,主執行緒就會去讀取任務佇列,依次執行那些滿足觸發條件的回撥函式。

以演講中的示例進一步說明

以圖中程式碼為例,執行引擎開始執行上述程式碼時,相當於先講一個main()方法加入執行棧。繼續往下開始console.log(‘Hi’)時,log(‘Hi’)方法入棧,console.log方法是一個webkit核心支援的普通方法,而不是前面圖中WebAPIs涉及的方法,所以這裡log(‘Hi’)方法立即出棧被引擎執行。

console.log(‘Hi’)語句執行完成後,log()方法出棧執行,輸出了Hi。引擎繼續往下,將setTimeout(callback,5000)新增到執行棧。setTimeout()方法屬於事件迴圈模型中WebAPIs中的方法,引擎在將setTimeout()方法出棧執行時,將延時執行的函式交給了相應模組,即圖右方的timer模組來處理。

執行引擎將setTimeout出棧執行時,將延時處理方法交由了webkit timer模組處理,然後立即繼續往下處理後面程式碼,於是將log(‘SJS’)加入執行棧,接下來log(‘SJS’)出棧執行,輸出SJS。而執行引擎在執行萬console.log(‘SJS’)後,程式處理完畢,main()方法也出棧。

這時在在setTimeout方法執行5秒後,timer模組檢測到延時處理方法到達觸發條件,於是將延時處理方法加入任務佇列。而此時執行引擎的執行棧為空,所以引擎開始輪詢檢查任務佇列是否有任務需要被執行,就檢查到已經到達執行條件的延時方法,於是將延時方法加入執行棧。引擎發現延時方法呼叫了log()方法,於是又將log()方法入棧。然後對執行棧依次出棧執行,輸出there,清空執行棧。

清空執行棧後,執行引擎會繼續去輪詢任務佇列,檢查是否還有任務可執行。

5.webkit中timer的實現

到這裡已經可以徹底理解下面程式碼的執行流程,執行引擎先將setTimeout()方法入棧被執行,執行時將延時方法交給核心相應模組處理。引擎繼續處理後面程式碼,while語句將引擎阻塞了1秒,而在這過程中,核心timer模組在0.5秒時已將延時方法新增到任務佇列,在引擎執行棧清空後,引擎將延時方法入棧並處理,最終輸出的時間超過預期設定的時間。

前面事件迴圈模型圖中提到的WebAPIs部分,提到了DOM事件,AJAX呼叫和setTimeout方法,圖中簡單的把它們總結為WebAPIs,而且他們同樣都把回撥函式新增到任務佇列等待引擎執行。這是一個簡化的描述,實際上瀏覽器核心對DOM事件、AJAX呼叫和setTimeout方法都有相應的模組來處理,webkit核心在Javasctipt執行引擎之外,有一個重要的模組是webcore模組,html的解析,css樣式的計算等都由webcore實現。對於圖中WebAPIs提到的三種API,webcore分別提供了DOM Binding、network、timer模組來處理底層實現,這裡還是繼續以setTimeout為例,看下timer模組的實現。

Timer類是webkit 核心的一個必需的基礎元件,通過閱讀原始碼可以全面理解其原理,本文對其簡化,分析其執行流程。

通過setTimeout()方法註冊的延時方法,被傳遞給webcore元件timer模組處理。timer中關鍵類為TheadTimers類,其包含兩個重要成員,TimerHeap任務佇列和SharedTimer方法排程類。延時方法被封裝為timer物件,儲存在TimerHeap中。和Java.util.Timer任務佇列一樣,TimerHeap同樣採用最小堆的資料結構,以nextFireTime作為關鍵字排序。SharedTimer作為TimerHeap排程類,在timer物件到達觸發條件時,通過瀏覽器平臺相關的介面,將延時方法新增到事件迴圈模型中提到的任務佇列中。

TimerHeap採用最小堆的資料結構,預期延時時間最小的任務最先被執行,同時,預期延時時間相同的兩個任務,其執行順序是按照註冊的先後順序執行。

上述程式碼輸出依次為

參考資料

1.《Javascript非同步程式設計》

2.JavaScript 執行機制詳解:再談Event Loophttp://www.ruanyifeng.com/blog/2014/10/event-loop.html

3.Philip Roberts: Help, I’m stuck in an event-loop.https://vimeo.com/96425312

4.How JavaScript Timers Work.http://ejohn.org/blog/how-javascript-timers-work/

5.How WebKit’s event model works.http://brrian.tumblr.com/post/13951629341/how-webkits-event-model-works

6.Timer實現.http://blog.csdn.net/shunzi__1984/article/details/6193023

相關文章