JavaScript引擎又稱爲JavaScript解釋器,是JavaScript解釋爲機器碼的工具,分別運行在瀏覽器和Node中。而根據上下文的不一樣,Event loop也有不一樣的實現:其中Node使用了libuv庫來實現Event loop; 而在瀏覽器中,html規範定義了Event loop,具體的實現則交給不一樣的廠商去完成。javascript
根據2017年新版的HTML規範HTML Standard,瀏覽器包含2類事件循環:browsing contexts 和 web workers。 html
browsing contexts中有一個或多個Task Queue,即MacroTask Queue,僅有一個Job Queue,即MicroTask Queue。html5
macrotask queue(宏任務,不妨稱爲A
)java
microtask queue(微任務,不妨稱爲I
)node
這兩個任務隊列執行順序:git
A
中的task,執行之。I
順序執行完,再取A
中的下一個任務。
爲何promise.then的回調比setTimeout先執行
代碼開始執行時,全部這些代碼在A
中,造成一個執行棧(execution context stack),取出來執行之。
遇到setTimeout,則加到A
中,遇到promise.then,則加到I
中。
等整個執行棧執行完,取I
中的任務。github
(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })() // 1 // 2 // 3 // 5 // 4
//瀏覽器渲染步驟:Structure(構建 DOM) ->Layout(排版)->Paint(繪製) //新的異步任務將在下一次被執行,所以就不會存在阻塞。 button.addEventListener('click', () => { setTimeout(fn, 0) })
V8源碼
https://github.com/v8/v8/blob...
https://github.com/v8/v8/blob...web
而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。c#
node新加了一個微任務process.nextTick
和一個宏任務setImmediate
.segmentfault
在當前"執行棧"的尾部(下一次Event Loop以前)觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前。
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0) // 1 // 2 // TIMEOUT FIRED
setImmediate
方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務老是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。
setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); //不肯定
遞歸的調用process.nextTick()會致使I/O starving,官方推薦使用setImmediate()
process.nextTick(function foo() { process.nextTick(foo); }); //FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory
process.nextTick也會放入microtask quque,爲何優先級比promise.then高呢
在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被調用,而這個_tickCallback中實質上幹了兩件事:
node.js的特色是事件驅動,非阻塞單線程。當應用程序須要I/O操做的時候,線程並不會阻塞,而是把I/O操做交給底層庫(LIBUV)。此時node線程會去處理其餘任務,當底層庫處理完I/O操做後,會將主動權交還給Node線程,因此Event Loop的用處是調度線程,例如:當底層庫處理I/O操做後調度Node線程處理後續工做,因此雖然node是單線程,可是底層庫處理操做依然是多線程。
根據Node.js官方介紹,每次事件循環都包含了6個階段,對應到 libuv 源碼中的實現,以下圖所示
timers :這個階段執行timer(setTimeout、setInterval)的回調
I/O callbacks:執行一些系統調用錯誤,好比網絡通訊的錯誤回調
idle, prepare :僅node內部使用
poll :獲取新的I/O事件, 適當的條件下node將阻塞在這裏
check :執行 setImmediate() 的回調
close callbacks :執行 socket 的 close 事件回調
timers 是事件循環的第一個階段,Node 會去檢查有無已過時的timer,若是有則把它的回調壓入timer的任務隊列中等待執行,事實上,Node 並不能保證timer在預設時間到了就會當即執行,由於Node對timer的過時檢查不必定靠譜,它會受機器上其它運行程序影響,或者那個時間點主線程不空閒。可是把它們放到一個I/O回調裏面,就必定是 setImmediate() 先執行,由於poll階段後面就是check階段。
這個階段主要執行一些系統操做帶來的回調函數,如 TCP 錯誤,若是 TCP 嘗試連接時出現 ECONNREFUSED 錯誤 ,一些 *nix 會把這個錯誤報告給 Node.js。而這個錯誤報告會先進入隊列中,而後在 I/O callbacks 階段執行。
poll 階段主要有2個功能:
even loop將同步執行poll隊列裏的回調,直到隊列爲空或執行的回調達到系統上限(上限具體多少未詳),接下來even loop會去檢查有無預設的setImmediate(),分兩種狀況:
注意一個細節,沒有setImmediate()會致使event loop阻塞在poll階段,這樣以前設置的timer豈不是執行不了了?因此咧,在poll階段event loop會有一個檢查機制,檢查timer隊列是否爲空,若是timer隊列非空,event loop就開始下一輪事件循環,即從新進入到timer階段。
setImmediate()的回調會被加入check隊列中, 從event loop的階段圖能夠知道,check階段的執行順序在poll階段以後。
忽然結束的事件的回調函數會在這裏觸發,若是 socket.destroy(),那麼 close 會被觸發在這個階段,也有可能經過 process.nextTick() 來觸發。
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) /*瀏覽器中 timer1 promise1 timer2 promise2 */ /*node中 timer1 timer2 promise1 promise2 */
const fs = require('fs') fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }) /* readFile immediate timeout */
更多示例
libuv源碼
https://github.com/libuv/libu...
HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。另外,對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般不會當即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()
客戶端可能實現了一個包含鼠標鍵盤事件的任務隊列,還有其餘的任務隊列,而給鼠標鍵盤事件的任務隊列更高優先級,例如75%的可能性執行它。這樣就能保證流暢的交互性,並且別的任務也能執行到了。可是,同一個任務隊列中的任務必須按先進先出的順序執行。
用戶點擊與button.click()的區別:
用戶點擊:依次執行listener。瀏覽器並不實現知道有幾個 listener,所以它發現一個執行一個,執行完了再看後面還有沒有。
click:同步執行listener。 click方法會先採集有哪些 listener,再依次觸發。
示例詳情
參考資料
Promise的隊列與setTimeout的隊列有何關聯?
瀏覽器的 Event Loop
Event Loops
深刻理解js事件循環機制(Node.js篇)
JavaScript 運行機制詳解:再談Event Loop
Node.js 事件循環,定時器和 process.nextTick()