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