Event Loop
即事件循環,是指瀏覽器或Node
的一種解決javaScript
單線程運行時不會阻塞的一種機制,也就是我們經常使用異步的原理。
Node
中的Event Loop
是基于libuv
實現的,而libuv
是 Node
的新跨平臺抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv的API
包含有時間,非阻塞的網絡,異步文件操作,子進程等等。 Event Loop
就是在libuv
中實現的。
Node
的Event loop
一共分為6個階段,每個細節具體如下:
timers
: 執行setTimeout
和setInterval
中到期的callback
。pending callback
: 上一輪循環中少數的callback
會放在這一階段執行。idle, prepare
: 僅在內部使用。poll
: 最重要的階段,執行pending callback
,在適當的情況下回阻塞在這個階段。check
: 執行setImmediate
(setImmediate()
是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成之后立即執行setImmediate
指定的回調函數)的callback
。close callbacks
: 執行close
事件的callback
,例如socket.on('close'[,fn])
或者http.server.on('close, fn)
。
階段
timers
執行setTimeout
和setInterval
中到期的callback
,執行這兩者回調需要設置一個毫秒數,理論上來說,應該是時間一到就立即執行callback回調,但是由于system
的調度可能會延時,達不到預期時間。
pending callbacks
此階段執行某些系統操作(例如TCP錯誤類型)的回調。 例如,如果TCP socket ECONNREFUSED
在嘗試connect時receives,則某些* nix系統希望等待報告錯誤。 這將在pending callbacks
階段執行。
poll
該poll階段有兩個主要功能:
執行
I/O
回調。處理輪詢隊列中的事件。
當事件循環進入poll
階段并且在timers
中沒有可以執行定時器時,將發生以下兩種情況之一
- 如果
poll
隊列不為空,則事件循環將遍歷其同步執行它們的callback
隊列,直到隊列為空,或者達到system-dependent
(系統相關限制)。
如果poll
隊列為空,則會發生以下兩種情況之一
如果有
setImmediate()
回調需要執行,則會立即停止執行poll
階段并進入執行check
階段以執行回調。如果沒有
setImmediate()
回到需要執行,poll階段將等待callback
被添加到隊列中,然后立即執行。
當然設定了 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執行回調。
check
此階段允許人員在poll階段完成后立即執行回調。 如果poll
階段閑置并且script
已排隊setImmediate()
,則事件循環到達check階段執行而不是繼續等待。
setImmediate()
實際上是一個特殊的計時器,它在事件循環的一個單獨階段運行。它使用libuv API
來調度在poll
階段完成后執行的回調。
通常,當代碼被執行時,事件循環最終將達到poll
階段,它將等待傳入連接,請求等。 但是,如果已經調度了回調setImmediate()
,并且輪詢階段變為空閑,則它將結束并且到達check
階段,而不是等待poll
事件。
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
復制代碼
如果node
版本為v11.x
, 其結果與瀏覽器一致。
start
end
promise3
timer1
promise1
timer2
promise2
如果v10版本上述結果存在兩種情況:
- 如果time2定時器已經在執行隊列中了
start
end
promise3
timer1
timer2
promise1
promise2
復制代碼
- 如果time2定時器沒有在執行對列中,執行結果為
start
end
promise3
timer1
promise1
timer2
promise2
復制代碼
具體情況可以參考poll
階段的兩種情況。
Node 與瀏覽器的 Event Loop 差異
瀏覽器環境下,microtask 的任務隊列是每個 macrotask 執行完之后執行。而在 Node.js 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。
通過一個例子來說明兩者區別:
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
全局腳本(main())執行,將 2 個 timer 依次放入 timer 隊列,main()執行完畢,調用棧空閑,任務隊列開始執行;
首先進入 timers 階段,執行 timer1 的回調函數,打印 timer1,并將 promise1.then 回調放入 microtask 隊列,同樣的步驟執行 timer2,打印 timer2;
至此,timer 階段執行結束,event loop 進入下一個階段之前,執行 microtask 隊列的所有任務,依次打印 promise1、promise2
參考文章:
https://juejin.im/post/5c3d8956e51d4511dc72c200?utm_source=gold_browser_extension
https://blog.csdn.net/weixin_34256074/article/details/88678373