【轉向Javascript系列】從setTimeout說事件循環模型

  本文首發在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

1.從setTimeout提及

  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。編程

2.Java對setTimeout的實現

  聯想起以往學習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中下一個任務開始執行的時間,會影響整個任務執行的實時性。爲了提升實時性,能夠採用多個消費者一塊兒消費來提升處理效率,避免此類問題。

3.根據結果找緣由

       看過了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#的機制很不一樣。

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秒時已將延時方法添加到任務隊列,在引擎執行棧清空後,引擎將延時方法入棧並處理,最終輸出的時間超過預期設置的時間。

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

相關文章
相關標籤/搜索