node.js極速入門課程:進入學習
ALL THE TIME,我們寫的的大部分javascript
程式碼都是在瀏覽器環境下編譯執行的,因此可能我們對瀏覽器的事件迴圈機制瞭解比Node.JS
的事件迴圈更深入一些,但是最近寫開始深入NodeJS學習的時候,發現NodeJS的事件迴圈機制和瀏覽器端有很大的區別,特此記錄來深入的學習了下,以幫助自己及小夥伴們忘記後查閱及理解。
首先我們需要了解一下最基礎的一些東西,比如這個事件迴圈,事件迴圈是指Node.js執行非阻塞I/O操作,儘管==JavaScript是單執行緒的==,但由於大多數==核心都是多執行緒==的,Node.js
會盡可能將操作裝載到系統核心。因此它們可以處理在後臺執行的多個操作。當其中一個操作完成時,核心會告訴Node.js
,以便Node.js
可以將相應的回撥新增到輪詢佇列中以最終執行。【相關教學推薦:】
當Node.js啟動時會初始化event loop
, 每一個event loop
都會包含按如下順序六個迴圈階段:
┌───────────────────────┐
┌─>│ 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會在這個階段執行;這個圖是整個 Node.js 的執行原理,從左到右,從上到下,Node.js 被分為了四層,分別是 應用層
、V8引擎層
、Node API層
和 LIBUV層
。
- 應用層: 即 JavaScript 互動層,常見的就是 Node.js 的模組,比如 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 互動
- NodeAPI層: 為上層模組提供系統呼叫,一般是由 C 語言來實現,和作業系統進行互動 。
- LIBUV層: 是跨平臺的底層封裝,實現了 事件迴圈、檔案操作等,是 Node.js 實現非同步的核心 。
timers
階段 一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回撥。在指定時間過後,timers會盡可能早地執行回撥,但系統排程或者其它回撥的執行可能會延遲它們。
注意:技術上來說,poll 階段控制 timers 什麼時候執行。
注意:這個下限時間有個範圍:[1, 2147483647],如果設定的時間不在這個範圍,將被設定為1。
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會遍歷佇列並同步執行回撥,直到佇列清空或執行的回撥數到達系統上限;
如果 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: 9timers: 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: 5008ps:1.將定時器延遲時間改為5ms的時候,小於檔案讀取時間,那麼就會先監聽到timers階段有事件進入,從而進入到timers階段執行,執行完畢繼續進行事件迴圈check階段timers: 6read time: 50082.將定時器事件設定為0ms,會在進入到poll階段的時候發現timers階段已經有callback,那麼會直接執行,然後執行完畢在下一階段迴圈,執行check階段,poll佇列的回撥函數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階段之前執行
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階段執行,參考nodejs進階視訊講解:進入學習
由於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
登入後複製
大家呢,可以先看著程式碼,默默地在心底走一變程式碼,然後對比輸出的結果,當然最後三位,我個人認為是有點問題的,畢竟在主模組執行,大家的答案,最後三位可能會有偏差;
更多node相關知識,請存取:!
以上就是一文詳解Node.js中的事件迴圈的詳細內容,更多請關注TW511.COM其它相關文章!