js: 從setTimeout說事件循環模型

1、從setTimeout提及javascript

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

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

  調用setTimeout()方法以後,該方法返回一個數字,這個數字是計劃執行代碼的惟一標識符,能夠經過它來取消超時調用。java

  起初我對 setTimeout()的使用比較簡單,對其運行機理也沒有深刻的理解,直到看到下面代碼web

1 var start = new Date;
2 setTimeout(function(){
3 var end = new Date;
4 console.log('Time elapsed:', end - start, 'ms');
5 }, 500);
6 while (new Date - start < 1000) {};

   在我最初對setTimeout()的認識中,延時設置爲500ms,因此輸出應該爲Time elapsed: 500 ms。由於在直觀的理解中,Javascript執行引擎,在執行上述代碼過程當中,應當是一個由上往下的順序執行過程,setTimeout函數是先於while語句執行的。但是實際上,上述代碼運行屢次後,輸出至少是延遲了1000ms。瀏覽器

 2、根據結果找緣由數據結構

   經過閱讀代碼不難看出,setTimeout()方法執行在while()循環以前,它聲明瞭「但願」在500ms以後執行一次匿名函數,這一聲明,也即對匿名函數的註冊,在setTimeout()方法執行後當即生效。代碼最後一行的while循環會持續運行1000ms,經過setTimeout()方法註冊的匿名函數輸出的延遲時間老是大於1000ms,說明對這一匿名函數的實際調用被while()循環阻塞了,實際的調用在while()循環阻塞結束後才真正執行。多線程

  使用Timer在Java中實現上述邏輯,運行屢次,輸出都是Time elapsed: 501 ms。java對於定時任務的解決方案是經過多線程手段實現的,任務對象存儲在任務隊列,由專門的調度線程,在新的子線程中完成任務的執行。經過schedule()方法註冊一個異步任務時,調度線程在子線程當即開始工做,主線程不會阻塞任務的運行。ecmascript

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

3、事件循環模型

   在單線程的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,清空執行棧。

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

4、webkit中timer的實現

  到這裏已經能夠完全理解下面代碼的執行流程,執行引擎先將setTimeout()方法入棧被執行,執行時將延時方法交給內核相應模塊處理。引擎繼續處理後面代碼,while語句將引擎阻塞了1秒,而在這過程當中,內核timer模塊在0.5秒時已將延時方法添加到任務隊列,在引擎執行棧清空後,引擎將延時方法入棧並處理,最終輸出的時間超過預期設置的時間。

1 var start = new Date;
2 setTimeout(function(){
3 var end = new Date;
4 console.log('Time elapsed:', end - start, 'ms');
5 }, 500);
6 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採用最小堆的數據結構,預期延時時間最小的任務最早被執行,同時,預期延時時間相同的兩個任務,其執行順序是按照註冊的前後順序執行。

 1 var start = new Date;
 2 setTimeout(function(){
 3 console.log('fn1');
 4 }, 20);
 5 setTimeout(function(){
 6 console.log('fn2');
 7 }, 30);
 8 setTimeout(function(){
 9 console.log('another fn2');
10 }, 30);
11 setTimeout(function(){
12 console.log('fn3');
13 }, 10);
14 console.log('start while');
15 while (new Date - start < 1000) {};
16 console.log('end while');

  上述代碼輸出依次爲

1 start while
2 end while
3 fn3
4 fn1
5 fn2
6 another fn2

 

 

轉載自AlloyTeam:http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/

循環間隔:HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏 覽器都將最短間隔設爲10毫秒。另外,對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般不會當即執行,而是每16毫秒執行一次。這時使用 requestAnimationFrame()的效果要好於setTimeout()。

 轉載:http://www.ruanyifeng.com/blog/2014/10/event-loop.html

相關文章
相關標籤/搜索