js: 從setTimeout說事件循環模型


一、從setTimeout說起

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

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

  調用setTimeout()方法之后,該方法返回一個數字,這個數字是計划執行代碼的唯一標識符,可以通過它來取消超時調用。

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

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。

 二、根據結果找原因

   通過閱讀代碼不難看出,setTimeout()方法執行在while()循環之前,它聲明了“希望”在500ms之后執行一次匿名函數,這一聲明,也即對匿名函數的注冊,在setTimeout()方法執行后立即生效。代碼最后一行的while循環會持續運行1000ms,通過setTimeout()方法注冊的匿名函數輸出的延遲時間總是大於1000ms,說明對這一匿名函數的實際調用被while()循環阻塞了,實際的調用在while()循環阻塞結束后才真正執行。

  使用Timer在Java中實現上述邏輯,運行多次,輸出都是Time elapsed: 501 ms。java對於定時任務的解決方案是通過多線程手段實現的,任務對象存儲在任務隊列,由專門的調度線程,在新的子線程中完成任務的執行。通過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,清空執行棧。

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

四、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM