ALL THE TIME,咱們寫的的大部分javascript
代碼都是在瀏覽器環境下編譯運行的,所以可能咱們對瀏覽器的事件循環機制瞭解比Node.JS
的事件循環更深刻一些,可是最近寫開始深刻NodeJS學習的時候,發現NodeJS的事件循環機制和瀏覽器端有很大的區別,特此記錄來深刻的學習了下,以幫助本身及小夥伴們忘記後查閱及理解。javascript
參考資料:java
首先咱們須要瞭解一下最基礎的一些東西,好比這個事件循環,事件循環是指Node.js執行非阻塞I/O操做,儘管==JavaScript是單線程的==,但因爲大多數==內核都是多線程==的,Node.js
會盡量將操做裝載到系統內核。所以它們能夠處理在後臺執行的多個操做。當其中一個操做完成時,內核會告訴Node.js
,以便Node.js
能夠將相應的回調添加到輪詢隊列中以最終執行。node
當Node.js啓動時會初始化event loop
, 每個event loop
都會包含按以下順序六個循環階段:segmentfault
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
timers
階段: 這個階段執行 setTimeout(callback)
和 setInterval(callback)
預約的 callback;I/O callbacks
階段: 此階段執行某些系統操做的回調,例如TCP錯誤的類型。 例如,若是TCP套接字在嘗試鏈接時收到 ECONNREFUSED,則某些* nix系統但願等待報告錯誤。 這將操做將等待在==I/O回調階段==執行;idle, prepare
階段: 僅node內部使用;poll
階段: 獲取新的I/O事件, 例如操做讀取文件等等,適當的條件下node將阻塞在這裏;check
階段: 執行 setImmediate()
設定的callbacks;close callbacks
階段: 好比 socket.on(‘close’, callback)
的callback會在這個階段執行;應用層
、
V8引擎層
、
Node API層
和
LIBUV層
。
- 應用層: 即 JavaScript 交互層,常見的就是 Node.js 的模塊,好比 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互
- NodeAPI層: 爲上層模塊提供系統調用,通常是由 C 語言來實現,和操做系統進行交互 。
- LIBUV層: 是跨平臺的底層封裝,實現了 事件循環、文件操做等,是 Node.js 實現異步的核心 。
timers
階段 一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定時間事後,timers會盡量早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。promise
注意:技術上來講,poll 階段控制 timers 何時執行。瀏覽器
注意:這個下限時間有個範圍:[1, 2147483647],若是設定的時間不在這個範圍,將被設置爲1。bash
I/O callbacks
階段 這個階段執行一些系統操做的回調。好比TCP錯誤,如一個TCP socket在想要鏈接時收到ECONNREFUSED, 類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行. 名字會讓人誤解爲執行I/O回調處理程序, 實際上I/O回調會由poll階段處理.多線程
poll
階段 poll 階段有兩個主要功能:(1)執行下限時間已經達到的timers的回調,(2)而後處理 poll 隊列裏的事件。 當event loop進入 poll 階段,而且 沒有設定的 timers(there are no timers scheduled),會發生下面兩件事之一:異步
若是 poll 隊列不空,event loop會遍歷隊列並同步執行回調,直到隊列清空或執行的回調數到達系統上限;socket
若是 poll 隊列爲空,則發生如下兩件事之一:
可是,當event loop進入 poll 階段,而且 有設定的timers,一旦 poll 隊列爲空(poll 階段空閒狀態): event loop將檢查timers,若是有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段,並執行 timer 隊列。
check
階段 這個階段容許在 poll 階段結束後當即執行回調。若是 poll 階段空閒,而且有被setImmediate()設定的回調,event loop會轉到 check 階段而不是繼續等待。
setImmediate() 其實是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv
的API 來設定在 poll 階段結束後當即執行回調。
一般上來說,隨着代碼執行,event loop終將進入 poll 階段,在這個階段等待 incoming connection, request 等等。可是,只要有被setImmediate()設定了回調,一旦 poll 階段空閒,那麼程序將結束 poll 階段並進入 check 階段,而不是繼續等待 poll 事件們 (poll events)。
close callbacks
階段 若是一個 socket 或 handle 被忽然關掉(好比 socket.destroy()),close事件將在這個階段被觸發,不然將經過process.nextTick()觸發
// 事件循環自己至關於一個死循環,當代碼開始執行的時候,事件循環就已經啓動了
// 而後順序調用不一樣階段的方法
while(true){
// timer階段
timer()
// I/O callbacks階段
IO()
// idle階段
IDLE()
// poll階段
poll()
// check階段
check()
// close階段
close()
}
// 在一次循環中,當事件循環進入到某一階段,加入進入到check階段,忽然timer階段的事件就緒,也會等到當前此次循環結束,再去執行對應的timer階段的回調函數
// 下面看這裏例子
const fs = require('fs')
// timers階段
const startTime = Date.now();
setTimeout(() => {
const endTime = Date.now()
console.log(`timers: ${endTime - startTime}`)
}, 1000)
// poll階段(等待新的事件出現)
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循環將fs回調強制阻塞5000s
while(endTime - readFileStart < 5000){
endTime = Date.now()
}
})
// check階段
setImmediate(() => {
console.log('check階段')
})
/*控制檯打印 check階段 read time: 9 timers: 5008 經過上述結果進行分析, 1.代碼執行到定時器setTimeOut,目前timers階段對應的事件列表爲空,在1000s後纔會放入事件 2.事件循環進入到poll階段,開始不斷的輪詢監聽事件 3.fs模塊異步執行,根據文件大小,可能執行時間長短不一樣,這裏我使用的小文件,事件大概在9s左右 4.setImmediate執行,poll階段暫時未監測到事件,發現有setImmediate函數,跳轉到check階段執行check階段事件(打印check階段),第一次時間循環結束,開始下一輪事件循環 5.由於時間仍未到定時器截止時間,因此事件循環有一次進入到poll階段,進行輪詢 6.讀取文件完畢,fs產生了一個事件進入到poll階段的事件隊列,此時事件隊列準備執行callback,因此會打印(read time: 9),人工阻塞了5s,雖然此時timer定時器事件已經被添加,可是由於這一階段的事件循環爲完成,因此不會被執行,(若是這裏是死循環,那麼定時器代碼永遠沒法執行) 7.fs回調阻塞5s後,當前事件循環結束,進入到下一輪事件循環,發現timer事件隊列有事件,因此開始執行 打印timers: 5008 ps: 1.將定時器延遲時間改成5ms的時候,小於文件讀取時間,那麼就會先監聽到timers階段有事件進入,從而進入到timers階段執行,執行完畢繼續進行事件循環 check階段 timers: 6 read time: 5008 2.將定時器事件設置爲0ms,會在進入到poll階段的時候發現timers階段已經有callback,那麼會直接執行,而後執行完畢在下一階段循環,執行check階段,poll隊列的回調函數 timers: 2 check階段 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階段以前執行
wait(20);
asyncOperation(() => {
console.log('poll');
});
});
/** * timers 21ms * poll */
複製代碼
這裏呢,爲了讓這個setTimeout
優先於fs.readFile
回調, 執行了process.nextTick
, 表示在進入timers
階段前, 等待20ms
後執行文件讀取.
nextTick
與 setImmediate
process.nextTick
不屬於事件循環的任何一個階段,它屬於該階段與下階段之間的過渡, 即本階段執行結束, 進入下一個階段前, 所要執行的回調。有給人一種插隊的感受.
setImmediate
的回調處於check階段, 當poll階段的隊列爲空, 且check階段的事件隊列存在的時候,切換到check階段執行.
因爲nextTick具備插隊的機制,nextTick的遞歸會讓事件循環機制沒法進入下一個階段. 致使I/O處理完成或者定時任務超時後仍然沒法執行, 致使了其它事件處理程序處於飢餓狀態. 爲了防止遞歸產生的問題, 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
階段去執行setTimeout裏面的回調方法
, 由於在進入timers
階段前有不斷的nextTick
插入執行. 除非執行了1000次到了執行上限,因此上面這個案例會不斷地打印出nextTick
字符串
setImmediate
若是在一個I/O週期
內進行調度,setImmediate() 將始終在任何定時器(setTimeout、setInterval)以前執行.
setTimeout
與 setImmediate
無 I/O 處理狀況下:
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
複製代碼
執行結果:
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
複製代碼
從結果,咱們能夠發現,這裏打印輸出出來的結果,並無什麼固定的前後順序,偏向於隨機,爲何會發生這樣的狀況呢?
答:首先進入的是timers
階段,若是咱們的機器性能通常,那麼進入timers
階段,1ms
已通過去了 ==(setTimeout(fn, 0)等價於setTimeout(fn, 1))==,那麼setTimeout
的回調會首先執行。
若是沒有到1ms
,那麼在timers
階段的時候,下限時間沒到,setTimeout
回調不執行,事件循環來到了poll
階段,這個時候隊列爲空,因而往下繼續,先執行了setImmediate()的回調函數,以後在下一個事件循環再執行setTimemout
的回調函數。
問題總結:而咱們在==執行啓動代碼==的時候,進入timers
的時間延遲實際上是==隨機的==,並非肯定的,因此會出現兩個函數執行順序隨機的狀況。
那咱們再來看一段代碼:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
打印結果以下:
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
# ... 省略 n 屢次使用 node test.js 命令 ,結果都輸出 immediate timeout
複製代碼
這裏,爲啥和上面的隨機timer
不一致呢,咱們來分析下緣由:
緣由以下:fs.readFile
的回調是在poll
階段執行的,當其回調執行完畢以後,poll
隊列爲空,而setTimeout
入了timers
的隊列,此時有代碼 setImmediate()
,因而事件循環先進入check
階段執行回調,以後在下一個事件循環再在timers
階段中執行回調。
固然,下面的小案例同理:
setTimeout(() => {
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
}, 0);
複製代碼
以上的代碼在timers
階段執行外部的setTimeout
回調後,內層的setTimeout
和setImmediate
入隊,以後事件循環繼續日後面的階段走,走到poll階段
的時候發現隊列爲空
,此時有代碼有setImmedate()
,因此直接進入check階段
執行響應回調(==注意這裏沒有去檢測timers隊列中是否有成員
到達下限事件,由於setImmediate()優先
==)。以後在第二個事件循環的timers
階段中再去執行相應的回調。
綜上所演示,咱們能夠總結以下:
setImmediate的回調永遠先執行
**。nextTick
與 Promise
概念:對於這兩個,咱們能夠把它們理解成一個微任務。也就是說,它其實不屬於事件循環的一部分。 那麼他們是在何時執行呢? 無論在什麼地方調用,他們都會在其所處的事件循環最後,事件循環進入下一個循環的階段前執行。
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:\Users\92809\Desktop\node_test>node test.js
timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout2
timeout resolved
複製代碼
最總結:timers
階段執行外層setTimeout
的回調,遇到同步代碼先執行,也就有timeout0
、sync
的輸出。遇到process.nextTick
及Promise
後入微任務隊列,依次nextTick1
、nextTick3
、nextTick2
、resolved
入隊後出隊輸出。以後,在下一個事件循環的timers
階段,執行setTimeout
回調輸出timeout2
以及微任務Promise
裏面的setTimeout
,輸出timeout resolved
。(這裏要說明的是 微任務nextTick
優先級要比Promise
要高)
代碼片斷1:
setImmediate(function(){
console.log("setImmediate");
setImmediate(function(){
console.log("嵌套setImmediate");
});
process.nextTick(function(){
console.log("nextTick");
})
});
/* C:\Users\92809\Desktop\node_test>node test.js setImmediate nextTick 嵌套setImmediate */
複製代碼
解析:
事件循環check
階段執行回調函數輸出setImmediate
,以後輸出nextTick
。嵌套的setImmediate
在下一個事件循環的check
階段執行回調輸出嵌套的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')
複製代碼
打印結果爲:
C:\Users\92809\Desktop\node_test>node test.js
script start
async1 start
async2
promise1
promise2
script end
nextTick
promise3
async1 end
setTimeout0
setTimeout3
setImmediate
複製代碼
你們呢,能夠先看着代碼,默默地在心底走一變代碼,而後對比輸出的結果,固然最後三位,我我的認爲是有點問題的,畢竟在主模塊運行,你們的答案,最後三位可能會有誤差;