本篇文章帶大家深度理解Node中的事件循環(huán),希望對大家有所幫助!
node.js極速入門課程:進入學(xué)習(xí)
ALL THE TIME,我們寫的的大部分javascript
代碼都是在瀏覽器環(huán)境下編譯運行的,因此可能我們對瀏覽器的事件循環(huán)機制了解比Node.JS
的事件循環(huán)更深入一些,但是最近寫開始深入NodeJS學(xué)習(xí)的時候,發(fā)現(xiàn)NodeJS的事件循環(huán)機制和瀏覽器端有很大的區(qū)別,特此記錄來深入的學(xué)習(xí)了下,以幫助自己及小伙伴們忘記后查閱及理解。
什么是事件循環(huán)
首先我們需要了解一下最基礎(chǔ)的一些東西,比如這個事件循環(huán),事件循環(huán)是指Node.js執(zhí)行非阻塞I/O操作,盡管==JavaScript是單線程的==,但由于大多數(shù)==內(nèi)核都是多線程==的,Node.js
會盡可能將操作裝載到系統(tǒng)內(nèi)核。因此它們可以處理在后臺執(zhí)行的多個操作。當(dāng)其中一個操作完成時,內(nèi)核會告訴Node.js
,以便Node.js
可以將相應(yīng)的回調(diào)添加到輪詢隊列中以最終執(zhí)行?!鞠嚓P(guān)教程推薦:nodejs視頻教程】
當(dāng)Node.js啟動時會初始化event loop
, 每一個event loop
都會包含按如下順序六個循環(huán)階段:
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
- 1.
timers
階段: 這個階段執(zhí)行setTimeout(callback)
和setInterval(callback)
預(yù)定的 callback; - 2.
I/O callbacks
階段: 此階段執(zhí)行某些系統(tǒng)操作的回調(diào),例如TCP錯誤的類型。 例如,如果TCP套接字在嘗試連接時收到 ECONNREFUSED,則某些* nix系統(tǒng)希望等待報告錯誤。 這將操作將等待在==I/O回調(diào)階段==執(zhí)行; - 3.
idle, prepare
階段: 僅node內(nèi)部使用; - 4.
poll
階段: 獲取新的I/O事件, 例如操作讀取文件等等,適當(dāng)?shù)臈l件下node將阻塞在這里; - 5.
check
階段: 執(zhí)行setImmediate()
設(shè)定的callbacks; - 6.
close callbacks
階段: 比如socket.on(‘close’, callback)
的callback會在這個階段執(zhí)行;
事件循環(huán)詳解
這個圖是整個 Node.js 的運行原理,從左到右,從上到下,Node.js 被分為了四層,分別是 應(yīng)用層
、V8引擎層
、Node API層
和 LIBUV層
。
- 應(yīng)用層: 即 JavaScript 交互層,常見的就是 Node.js 的模塊,比如 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互
- NodeAPI層: 為上層模塊提供系統(tǒng)調(diào)用,一般是由 C 語言來實現(xiàn),和操作系統(tǒng)進行交互 。
- LIBUV層: 是跨平臺的底層封裝,實現(xiàn)了 事件循環(huán)、文件操作等,是 Node.js 實現(xiàn)異步的核心 。
每個循環(huán)階段內(nèi)容詳解
timers
階段 一個timer指定一個下限時間而不是準確時間,在達到這個下限時間后執(zhí)行回調(diào)。在指定時間過后,timers會盡可能早地執(zhí)行回調(diào),但系統(tǒng)調(diào)度或者其它回調(diào)的執(zhí)行可能會延遲它們。
-
注意:技術(shù)上來說,poll 階段控制 timers 什么時候執(zhí)行。
-
注意:這個下限時間有個范圍:[1, 2147483647],如果設(shè)定的時間不在這個范圍,將被設(shè)置為1。
I/O callbacks
階段 這個階段執(zhí)行一些系統(tǒng)操作的回調(diào)。比如TCP錯誤,如一個TCP socket在想要連接時收到ECONNREFUSED, 類unix系統(tǒng)會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執(zhí)行. 名字會讓人誤解為執(zhí)行I/O回調(diào)處理程序, 實際上I/O回調(diào)會由poll階段處理.
poll
階段 poll 階段有兩個主要功能:(1)執(zhí)行下限時間已經(jīng)達到的timers的回調(diào),(2)然后處理 poll 隊列里的事件。 當(dāng)event loop進入 poll 階段,并且 沒有設(shè)定的 timers(there are no timers scheduled),會發(fā)生下面兩件事之一:
-
如果 poll 隊列不空,event loop會遍歷隊列并同步執(zhí)行回調(diào),直到隊列清空或執(zhí)行的回調(diào)數(shù)到達系統(tǒng)上限;
-
如果 poll 隊列為空,則發(fā)生以下兩件事之一:
- 如果代碼已經(jīng)被setImmediate()設(shè)定了回調(diào), event loop將結(jié)束 poll 階段進入 check 階段來執(zhí)行 check 隊列(里面的回調(diào) callback)。
- 如果代碼沒有被setImmediate()設(shè)定回調(diào),event loop將阻塞在該階段等待回調(diào)被加入 poll 隊列,并立即執(zhí)行。
-
但是,當(dāng)event loop進入 poll 階段,并且 有設(shè)定的timers,一旦 poll 隊列為空(poll 階段空閑狀態(tài)): event loop將檢查timers,如果有1個或多個timers的下限時間已經(jīng)到達,event loop將繞回 timers 階段,并執(zhí)行 timer 隊列。
check
階段 這個階段允許在 poll 階段結(jié)束后立即執(zhí)行回調(diào)。如果 poll 階段空閑,并且有被setImmediate()設(shè)定的回調(diào),event loop會轉(zhuǎn)到 check 階段而不是繼續(xù)等待。
-
setImmediate() 實際上是一個特殊的timer,跑在event loop中一個獨立的階段。它使用
libuv
的API 來設(shè)定在 poll 階段結(jié)束后立即執(zhí)行回調(diào)。 -
通常上來講,隨著代碼執(zhí)行,event loop終將進入 poll 階段,在這個階段等待 incoming connection, request 等等。但是,只要有被setImmediate()設(shè)定了回調(diào),一旦 poll 階段空閑,那么程序?qū)⒔Y(jié)束 poll 階段并進入 check 階段,而不是繼續(xù)等待 poll 事件們 (poll events)。
close callbacks
階段 如果一個 socket 或 handle 被突然關(guān)掉(比如 socket.destroy()),close事件將在這個階段被觸發(fā),否則將通過process.nextTick()觸發(fā)
這里呢,我們通過偽代碼來說明一下,這個流程:
// 事件循環(huán)本身相當(dāng)于一個死循環(huán),當(dāng)代碼開始執(zhí)行的時候,事件循環(huán)就已經(jīng)啟動了 // 然后順序調(diào)用不同階段的方法 while(true){ // timer階段 timer() // I/O callbacks階段 IO() // idle階段 IDLE() // poll階段 poll() // check階段 check() // close階段 close() } // 在一次循環(huán)中,當(dāng)事件循環(huán)進入到某一階段,加入進入到check階段,突然timer階段的事件就緒,也會等到當(dāng)前這次循環(huán)結(jié)束,再去執(zhí)行對應(yīng)的timer階段的回調(diào)函數(shù) // 下面看這里例子 const fs = require('fs') // timers階段 const startTime = Date.now(); setTimeout(() => { const endTime = Date.now() console.log(`timers: ${endTime - startTime}`) }, 1000) // poll階段(等待新的事件出現(xiàn)) const readFileStart = Date.now(); fs.readFile('./Demo.txt', (err, data) => { if (err) throw err let endTime = Date.now() // 獲取文件讀取的時間 console.log(`read time: ${endTime - readFileStart}`) // 通過while循環(huán)將fs回調(diào)強制阻塞5000s while(endTime - readFileStart < 5000){ endTime = Date.now() } }) // check階段 setImmediate(() => { console.log('check階段') }) /*控制臺打印check階段read time: 9timers: 5008通過上述結(jié)果進行分析,1.代碼執(zhí)行到定時器setTimeOut,目前timers階段對應(yīng)的事件列表為空,在1000s后才會放入事件2.事件循環(huán)進入到poll階段,開始不斷的輪詢監(jiān)聽事件3.fs模塊異步執(zhí)行,根據(jù)文件大小,可能執(zhí)行時間長短不同,這里我使用的小文件,事件大概在9s左右4.setImmediate執(zhí)行,poll階段暫時未監(jiān)測到事件,發(fā)現(xiàn)有setImmediate函數(shù),跳轉(zhuǎn)到check階段執(zhí)行check階段事件(打印check階段),第一次時間循環(huán)結(jié)束,開始下一輪事件循環(huán)5.因為時間仍未到定時器截止時間,所以事件循環(huán)有一次進入到poll階段,進行輪詢6.讀取文件完畢,fs產(chǎn)生了一個事件進入到poll階段的事件隊列,此時事件隊列準備執(zhí)行callback,所以會打印(read time: 9),人工阻塞了5s,雖然此時timer定時器事件已經(jīng)被添加,但是因為這一階段的事件循環(huán)為完成,所以不會被執(zhí)行,(如果這里是死循環(huán),那么定時器代碼永遠無法執(zhí)行)7.fs回調(diào)阻塞5s后,當(dāng)前事件循環(huán)結(jié)束,進入到下一輪事件循環(huán),發(fā)現(xiàn)timer事件隊列有事件,所以開始執(zhí)行 打印timers: 5008ps:1.將定時器延遲時間改為5ms的時候,小于文件讀取時間,那么就會先監(jiān)聽到timers階段有事件進入,從而進入到timers階段執(zhí)行,執(zhí)行完畢繼續(xù)進行事件循環(huán)check階段timers: 6read time: 50082.將定時器事件設(shè)置為0ms,會在進入到poll階段的時候發(fā)現(xiàn)timers階段已經(jīng)有callback,那么會直接執(zhí)行,然后執(zhí)行完畢在下一階段循環(huán),執(zhí)行check階段,poll隊列的回調(diào)函數(shù)timers: 2check階段read time: 7 */
走進案例解析
我們來看一個簡單的EventLoop
的例子:
const fs = require('fs'); let counts = 0; // 定義一個 wait 方法 function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } // 讀取本地文件 操作IO function asyncOperation (callback) { fs.readFile(__dirname + '/' + __filename, callback); } const lastTime = Date.now(); // setTimeout setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); // process.nextTick process.nextTick(() => { // 進入event loop // timers階段之前執(zhí)行 wait(20); asyncOperation(() => { console.log('poll'); }); }); /** * timers 21ms * poll */
這里呢,為了讓這個setTimeout
優(yōu)先于fs.readFile
回調(diào), 執(zhí)行了process.nextTick
, 表示在進入timers
階段前, 等待20ms
后執(zhí)行文件讀取.
1. nextTick
與 setImmediate
-
process.nextTick
不屬于事件循環(huán)的任何一個階段,它屬于該階段與下階段之間的過渡, 即本階段執(zhí)行結(jié)束, 進入下一個階段前, 所要執(zhí)行的回調(diào)。有給人一種插隊的感覺. -
setImmediate
的回調(diào)處于check階段, 當(dāng)poll階段的隊列為空, 且check階段的事件隊列存在的時候,切換到check階段執(zhí)行,參考nodejs進階視頻講解:進入學(xué)習(xí)
nextTick 遞歸的危害
由于nextTick具有插隊的機制,nextTick的遞歸會讓事件循環(huán)機制無法進入下一個階段. 導(dǎo)致I/O處理完成或者定時任務(wù)超時后仍然無法執(zhí)行, 導(dǎo)致了其它事件處理程序處于饑餓狀態(tài). 為了防止遞歸產(chǎn)生的問題, Node.js 提供了一個 process.maxTickDepth (默認 1000)。
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function nextTick () { process.nextTick(() => { wait(20); console.log('nextTick'); nextTick(); }); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); nextTick();
此時永遠無法跳到timer
階段去執(zhí)行setTimeout里面的回調(diào)方法
, 因為在進入timers
階段前有不斷的nextTick
插入執(zhí)行. 除非執(zhí)行了1000次到了執(zhí)行上限,所以上面這個案例會不斷地打印出nextTick
字符串
2. setImmediate
如果在一個I/O周期
內(nèi)進行調(diào)度,setImmediate() 將始終在任何定時器(setTimeout、setInterval)之前執(zhí)行.
3. setTimeout
與 setImmediate
- setImmediate()被設(shè)計在 poll 階段結(jié)束后立即執(zhí)行回調(diào);
- setTimeout()被設(shè)計在指定下限時間到達后執(zhí)行回調(diào);
無 I/O 處理情況下:
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
執(zhí)行結(jié)果:
C:Users92809Desktopnode_test>node test.js timeout immediate C:Users92809Desktopnode_test>node test.js timeout immediate C:Users92809Desktopnode_test>node test.js timeout immediate C:Users92809Desktopnode_test>node test.js immediate timeout
從結(jié)果,我們可以發(fā)現(xiàn),這里打印輸出出來的結(jié)果,并沒有什么固定的先后順序,偏向于隨機,為什么會發(fā)生這樣的情況呢?
答:首先進入的是timers
階段,如果我們的機器性能一般,那么進入timers
階段,1ms
已經(jīng)過去了 ==(setTimeout(fn, 0)等價于setTimeout(fn, 1))==,那么setTimeout
的回調(diào)會首先執(zhí)行。
如果沒有到1ms
,那么在timers
階段的時候,下限時間沒到,setTimeout
回調(diào)不執(zhí)行,事件循環(huán)來到了poll
階段,這個時候隊列為空,于是往下繼續(xù),先執(zhí)行了setImmediate()的回調(diào)函數(shù),之后在下一個事件循環(huán)再執(zhí)行setTimemout
的回調(diào)函數(shù)。
問題總結(jié):而我們在==執(zhí)行啟動代碼==的時候,進入timers
的時間延遲其實是==隨機的==,并不是確定的,所以會出現(xiàn)兩個函數(shù)執(zhí)行順序隨機的情況。
那我們再來看一段代碼:
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
打印結(jié)果如下:
C:Users92809Desktopnode_test>node test.js immediate timeout C:Users92809Desktopnode_test>node test.js immediate timeout C:Users92809Desktopnode_test>node test.js immediate timeout # ... 省略 n 多次使用 node test.js 命令 ,結(jié)果都輸出 immediate timeout
這里,為啥和上面的隨機timer
不一致呢,我們來分析下原因:
原因如下:fs.readFile
的回調(diào)是在poll
階段執(zhí)行的,當(dāng)其回調(diào)執(zhí)行完畢之后,poll
隊列為空,而setTimeout
入了timers
的隊列,此時有代碼 setImmediate()
,于是事件循環(huán)先進入check
階段執(zhí)行回調(diào),之后在下一個事件循環(huán)再在timers
階段中執(zhí)行回調(diào)。
當(dāng)然,下面的小案例同理:
setTimeout(() => { setImmediate(() => { console.log('setImmediate'); }); setTimeout(() => { console.log('setTimeout'); }, 0); }, 0);
以上的代碼在timers
階段執(zhí)行外部的setTimeout
回調(diào)后,內(nèi)層的setTimeout
和setImmediate
入隊,之后事件循環(huán)繼續(xù)往后面的階段走,走到poll階段
的時候發(fā)現(xiàn)隊列為空
,此時有代碼有setImmedate()
,所以直接進入check階段
執(zhí)行響應(yīng)回調(diào)(==注意這里沒有去檢測timers隊列中是否有成員
到達下限事件,因為setImmediate()優(yōu)先
==)。之后在第二個事件循環(huán)的timers
階段中再去執(zhí)行相應(yīng)的回調(diào)。
綜上所演示,我們可以總結(jié)如下:
- 如果兩者都在主模塊中調(diào)用,那么執(zhí)行先后取決于進程性能,也就是你的電腦好撇,當(dāng)然也就是隨機。
- 如果兩者都不在主模塊調(diào)用(被一個異步操作包裹),那么**
setImmediate的回調(diào)永遠先執(zhí)行
**。
4. nextTick
與 Promise
概念:對于這兩個,我們可以把它們理解成一個微任務(wù)。也就是說,它其實不屬于事件循環(huán)的一部分。 那么他們是在什么時候執(zhí)行呢? 不管在什么地方調(diào)用,他們都會在其所處的事件循環(huán)最后,事件循環(huán)進入下一個循環(huán)的階段前執(zhí)行。
setTimeout(() => { console.log('timeout0'); new Promise((resolve, reject) => { resolve('resolved') }).then(res => console.log(res)); new Promise((resolve, reject) => { setTimeout(()=>{ resolve('timeout resolved') }) }).then(res => console.log(res)); process.nextTick(() => { console.log('nextTick1'); process.nextTick(() => { console.log('nextTick2'); }); }); process.nextTick(() => { console.log('nextTick3'); }); console.log('sync'); setTimeout(() => { console.log('timeout2'); }, 0); }, 0);
控制臺打印如下:
C:Users92809Desktopnode_test>node test.js timeout0 sync nextTick1 nextTick3 nextTick2 resolved timeout2 timeout resolved
最總結(jié):timers
階段執(zhí)行外層setTimeout
的回調(diào),遇到同步代碼先執(zhí)行,也就有timeout0
、sync
的輸出。遇到process.nextTick
及Promise
后入微任務(wù)隊列,依次nextTick1
、nextTick3
、nextTick2
、resolved
入隊后出隊輸出。之后,在下一個事件循環(huán)的timers
階段,執(zhí)行setTimeout
回調(diào)輸出timeout2
以及微任務(wù)Promise
里面的setTimeout
,輸出timeout resolved
。(這里要說明的是 微任務(wù)nextTick
優(yōu)先級要比Promise
要高)
5. 最后案例
代碼片段1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); /* C:Users92809Desktopnode_test>node test.js setImmediate nextTick 嵌套setImmediate*/
解析:
事件循環(huán)check
階段執(zhí)行回調(diào)函數(shù)輸出setImmediate
,之后輸出nextTick
。嵌套的setImmediate
在下一個事件循環(huán)的check
階段執(zhí)行回調(diào)輸出嵌套的setImmediate
。
代碼片段2:
async function async1(){ console.log('async1 start') await async2() console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start') setTimeout(function(){ console.log('setTimeout0') },0) setTimeout(function(){ console.log('setTimeout3') },3) setImmediate(() => console.log('setImmediate')); process.nextTick(() => console.log('nextTick')); async1(); new Promise(function(resolve){ console.log('promise1') resolve(); console.log('promise2') }).then(function(){ console.log('promise3') }) console.log('script end')
打印結(jié)果為:
C:Users92809Desktopnode_test>node test.js script start async1 start async2 promise1 promise2 script end nextTick promise3 async1 end setTimeout0 setTimeout3 setImmediate
大家呢,可以先看著代碼,默默地在心底走一變代碼,然后對比輸出的結(jié)果,當(dāng)然最后三位,我個人認為是有點問題的,畢竟在主模塊運行,大家的答案,最后三位可能會有偏差;