本文首發在alloyteam團隊博客,連接地址http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/javascript
做爲一個從其餘編程語言(C#/Java)轉到Javascript的開發人員,在學習Javascript過程當中,setTimeout()運行原理是我遇到的一個不太好理解的部分,本文嘗試結合其餘編程語言的實現,從setTimeout說事件循環模型css
setTimeout()方法不是ecmascript規範定義的內容,而是屬於BOM提供的功能。查看w3school對setTimeout()方法的定義,setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。html
語法setTimeout(fn,millisec),其中fn表示要執行的代碼,能夠是一個包含javascript代碼的字符串,也能夠是一個函數。第二個參數millisec是以毫秒錶示的時間,表示fn需推遲多長時間執行。前端
調用setTimeout()方法以後,該方法返回一個數字,這個數字是計劃執行代碼的惟一標識符,能夠經過它來取消超時調用。java
起初我對 setTimeout()的使用比較簡單,對其運行機理也沒有深刻的理解,直到看到下面代碼web
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
在我最初對setTimeout()的認識中,延時設置爲500ms,因此輸出應該爲Time elapsed: 500 ms。由於在直觀的理解中,Javascript執行引擎,在執行上述代碼過程當中,應當是一個由上往下的順序執行過程,setTimeout函數是先於while語句執行的。但是實際上,上述代碼運行屢次後,輸出至少是延遲了1000ms。編程
聯想起以往學習Java的經驗,上述Javascript的setTimeout()讓我困惑。Java對setTimeout的實現有多種API實現,這裏咱們以java.util.Timer包爲例。使用Timer在Java中實現上述邏輯,運行屢次,輸出都是Time elapsed: 501 ms。vim
import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) { // TODO Auto-generated method stub long start = System.currentTimeMillis(); Timer timer = new Timer(); timer.schedule(new MyTask(start), 500); while (System.currentTimeMillis() - start < 1000) {}; } } class MyTask extends TimerTask { private long t; public MyTask(long start) { // TODO Auto-generated constructor stub t=start; } @Override public void run() { // TODO Auto-generated method stub long end = System.currentTimeMillis(); System.out.println("Time elapsed:"+(end - this.t)+ "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最小的任務在隊列的最前端,從而可以現實最先執行。
根據上述源碼分析能夠總結出如上如所示的流程圖,實際上,這是一個多生產者、單一消費者的生產者--消費者模型。此種方式的不足之處爲當某個任務執行時間較長,超過了TaskQueue中下一個任務開始執行的時間,會影響整個任務執行的實時性。爲了提升實時性,能夠採用多個消費者一塊兒消費來提升處理效率,避免此類問題。
看過了Java.util.Timer對相似setTimeout()的實現方案,繼續回到前文Javascript的setTimeout()方法中,再來看看以前的輸出爲何與預期不符。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
經過閱讀代碼不難看出,setTimeout()方法執行在while()循環以前,它聲明瞭「但願」在500ms以後執行一次匿名函數,這一聲明,也即對匿名函數的註冊,在setTimeout()方法執行後當即生效。代碼最後一行的while循環會持續運行1000ms,經過setTimeout()方法註冊的匿名函數輸出的延遲時間老是大於1000ms,說明對這一匿名函數的實際調用被while()循環阻塞了,實際的調用在while()循環阻塞結束後才真正執行。
而在Java.util.Timer中,對於定時任務的解決方案是經過多線程手段實現的,任務對象存儲在任務隊列,由專門的調度線程,在新的子線程中完成任務的執行。經過schedule()方法註冊一個異步任務時,調度線程在子線程當即開始工做,主線程不會阻塞任務的運行。
這就是Javascript與Java/C#之類語言的一大差別,即Javascript的單線程機制。在現有瀏覽器環境中,Javascript執行引擎是單線程的,主線程的語句和方法,會阻塞定時任務的運行,執行引擎只有在執行完主線程的語句後,定時任務纔會實際執行,這期間的時間,可能大於註冊任務時設置的延時時間。在這一點上,Javascript與Java/C#的機制很不一樣。
在單線程的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,清空執行棧。
清空執行棧後,執行引擎會繼續去輪詢任務隊列,檢查是否還有任務可執行。
到這裏已經能夠完全理解下面代碼的執行流程,執行引擎先將setTimeout()方法入棧被執行,執行時將延時方法交給內核相應模塊處理。引擎繼續處理後面代碼,while語句將引擎阻塞了1秒,而在這過程當中,內核timer模塊在0.5秒時已將延時方法添加到任務隊列,在引擎執行棧清空後,引擎將延時方法入棧並處理,最終輸出的時間超過預期設置的時間。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
前面事件循環模型圖中提到的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採用最小堆的數據結構,預期延時時間最小的任務最早被執行,同時,預期延時時間相同的兩個任務,其執行順序是按照註冊的前後順序執行。
var start = new Date; setTimeout(function(){ console.log('fn1'); }, 20); setTimeout(function(){ console.log('fn2'); }, 30); setTimeout(function(){ console.log('another fn2'); }, 30); setTimeout(function(){ console.log('fn3'); }, 10); console.log('start while'); while (new Date - start < 1000) {}; console.log('end while');
上述代碼輸出依次爲
start while end while fn3 fn1 fn2 another fn2
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