主執行緒從"任務佇列"中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop(事件迴圈)。下面本篇文章就來帶大家掌握Node.js中的eventloop,希望對大家有所幫助!
雖然js
可以在瀏覽器中執行又可以在node
中執行,但是它們的事件迴圈機制並不是一樣的。並且有很大的區別。
在說Node
事件迴圈機制之前,我們先來討論兩個問題
學習事件迴圈可以讓開發者明白JavaScript
的執行機制是怎麼樣的。
事件迴圈機制用於管理非同步API的回撥函數什麼時候回到主執行緒中執行。
Node.js採用的是非同步IO模型。同步API在主執行緒中執行,非同步API在底層的C++維護的執行緒中執行,非同步API的回撥函數也會在主執行緒中執行。【相關教學推薦:、】
在Javascript應用執行時,眾多非同步API的回撥函數什麼時候能回到主執行緒中呼叫呢?這就是事件環環機制做的事情,管理非同步API的回撥函數什麼時候回到主執行緒中執行。
在Node
中的事件迴圈分為六個階段。
在事件迴圈中的每個階段都有一個佇列,儲存要執行的回撥函數,事件迴圈機制會按照先進先出的方式執行他們直到佇列為空。
這六個階段都儲存著非同步回撥函數,所以還是遵循先執行主執行緒同步程式碼,當同步程式碼執行完後再來輪詢這六個階段。
接下來,我們來詳細看看這六個階段裡面儲存的都是什麼
Timers
:用於儲存定時器的回撥函數(setlnterval,setTimeout)。
Pendingcallbacks
:執行與作業系統相關的回撥函數,比如啟動伺服器端應用時監聽埠操作的回撥函數就在這裡呼叫。
idle,prepare
:系統內部使用。(這個我們程式設計師不用管)
Poll
:儲存1/O操作的回撥函數佇列,比如檔案讀寫操作的回撥函數。
在這個階段需要特別注意,如果事件佇列中有回撥函數,則執行它們直到清空佇列 ,否則事件迴圈將在此階段停留一段時間以等待新的回撥函數進入。
但是對於這個等待並不是一定的,而是取決於以下兩個條件:
Check
:儲存setlmmediate的回撥函數。
Closingcallbacks
:執行與關閉事件相關的回撥,例如關閉資料庫連線的回撥函數等。
跟瀏覽器中的js
一樣,node
中的非同步程式碼也分為宏任務和微任務,只是它們之間的執行順序有所區別。
我們再來看看Node
中都有哪些宏任務和微任務
setlnterval
setimeout
setlmmediate
I/O
Promise.then
Promise.catch
Promise.finally
process.nextTick
在node
中,對於微任務和宏任務的執行順序到底是怎樣的呢?
在node
中,微任務的回撥函數被放置在微任務佇列中,宏任務的回撥函數被放置在宏任務佇列中。
微任務優先順序高於宏任務。當微任務事件佇列中存在可以執行的回撥函數時,事件迴圈在執行完當前階段的回撥函數後會暫停進入事件迴圈的下一個階段,而會立即進入微任務的事件佇列中開始執行回撥函數,當微任務佇列中的回撥函數執行完成後,事件迴圈才會進入到下一個段開始執行回撥函數。
對於微任務我們還有個點需要特別注意。那就是雖然nextTick
同屬於微任務,但是它的優先順序是高於其它微任務,在執行微任務時,只有nextlick
中的所有回撥函數執行完成後才會開始執行其它微任務。
總的來說就是當主執行緒同步程式碼執行完畢後會優先清空微任務(如果微任務繼續產生微任務則會再次清空),然後再到下個事件迴圈階段。並且微任務的執行是穿插在事件迴圈六個階段中間的,也就是每次事件迴圈進入下個階段前會判斷微任務佇列是否為空,為空才會進入下個階段,否則先清空微任務佇列。
下面我們用程式碼實操來驗證前面所說的。
在Node
應用程式啟動後,並不會立即進入事件迴圈,而是先執行同步程式碼,從上到下開始執行,同步API立即執行,非同步API交給C++維護的執行緒執行,非同步API的回撥函數被註冊到對應的事件佇列中。當所有同步程式碼執行完成後,才會進入事件迴圈。
console.log("start");
setTimeout(() => {
console.log("setTimeout 1");
});
setTimeout(() => {
console.log("setTimeout 2");
});
console.log("end");
登入後複製
我們來看執行結果
可以看到,先執行同步程式碼,然後才會進入事件迴圈執行非同步程式碼,在timers
階段執行兩個setTimeout
回撥。
我們知道setTimeout
是在timers
階段執行,setImmediate
是在check
階段執行。並且事件迴圈是從timers
階段開始的。所以會先執行setTimeout
再執行setImmediate
。
對於上面的分析一定對嗎?
我們來看例子
console.log("start");
setTimeout(() => {
console.log("setTimeout");
});
setImmediate(() => {
console.log("setImmediate");
});
const sleep = (delay) => {
const startTime = +new Date();
while (+new Date() - startTime < delay) {
continue;
}
};
sleep(2000);
console.log("end");
登入後複製
執行上面的程式碼,輸出如下
先執行setTimeout
再執行setImmediate
接下來我們來改造下上面的程式碼,把延遲器去掉,看看會輸出什麼
setTimeout(() => {
console.log("setTimeout");
});
setImmediate(() => {
console.log("setImmediate");
});
登入後複製
我們執行了七次,可以看到其中有兩次是先執行的setImmediate
怎麼回事呢?不是先timers
階段再到check
階段嗎?怎麼會變呢?
其實這就得看進入事件迴圈的時候,非同步回撥有沒有完全準備好了。對於最開始的例子,因為有2000毫秒的延遲,所以進入事件迴圈的時候,setTimeout
回撥是一定準備好了的。所以執行順序不會變。但是對於這個例子,因為主執行緒沒有同步程式碼需要執行,所以一開始就進入事件迴圈,但是在進入事件迴圈的時候,setTimeout
的回撥並不是一定完全準備好的,所以就會有先到check
階段執行setImmediate
回撥函數,再到下一次事件迴圈的timers
階段來執行setTimeout
的回撥。
那在什麼情況下同樣的延遲時間,setImmediate
回撥函數一定會優先於setTimeout
的回撥呢?
其實很簡單,只要將這兩者放到timers
階段和check
階段之間的Pendingcallbacks、idle,prepare、poll
階段中任意一個階段就可以了。因為這些階段完執行完是一定會先到check
再到timers
階段的。
我們以poll
階段為例,將這兩者寫在IO操作中。
const fs = require("fs");
fs.readFile("./fstest.js", "utf8", (err, data) => {
setTimeout(() => {
console.log("setTimeout");
});
setImmediate(() => {
console.log("setImmediate");
});
});
登入後複製
我們也來執行七次,可以看到,每次都是setImmediate
先執行。
所以總的來說,同樣的延遲時間,setTimeout
並不是百分百先於setImmediate
執行。
主執行緒同步程式碼執行完畢後,會先執行微任務再執行宏任務。
我們來看下面的例子
console.log("start");
setTimeout(() => {
console.log("setTimeout");
});
setImmediate(() => {
console.log("setImmediate");
});
Promise.resolve().then(() => {
console.log("Promise.resolve");
});
console.log("end");
登入後複製
我們執行一下看結果,可以看到它是先執行了微任務然後再執行宏任務
在微任務中nextTick
的優先順序是最高的。
我們來看下面的例子
console.log("start");
setTimeout(() => {
console.log("setTimeout");
});
setImmediate(() => {
console.log("setImmediate");
});
Promise.resolve().then(() => {
console.log("Promise.resolve");
});
process.nextTick(() => {
console.log("process.nextTick");
});
console.log("end");
登入後複製
我們執行上面的程式碼,可以看到就算nextTick
定義在resolve
後面,它也是先執行的。
怎麼理解這個穿插呢?其實就是在事件迴圈的六個階段每個階段執行完後會清空微任務佇列。
我們來看例子,我們建立了timers、check、poll
三個階段,並且每個階段都產生了微任務。
// timers階段
setTimeout(() => {
console.log("setTimeout");
Promise.resolve().then(() => {
console.log("setTimeout Promise.resolve");
});
});
// check階段
setImmediate(() => {
console.log("setImmediate");
Promise.resolve().then(() => {
console.log("setImmediate Promise.resolve");
});
});
// 微任務
Promise.resolve().then(() => {
console.log("Promise.resolve");
});
// 微任務
process.nextTick(() => {
console.log("process.nextTick");
Promise.resolve().then(() => {
console.log("nextTick Promise.resolve");
});
});
登入後複製
我們來執行上面的程式碼
可以看到,先執行微任務,再執行宏任務。先process.nextTick -> Promise.resolve
。並且如果微任務繼續產生微任務則會再次清空,所以就又輸出了nextTick Promise.resolve
。
接下來到timer
階段,輸出setTimeout
,並且產生了一個微任務,再進入到下個階段前需要清空微任務佇列,所以繼續輸出setTimeout Promise.resolve
。
接下來到check
階段,輸出setImmediate
,並且產生了一個微任務,再進入到下個階段前需要清空微任務佇列,所以繼續輸出setImmediate Promise.resolve
。
這也就印證了微任務會穿插在各個階段之間執行。
所以對於Node
中的事件迴圈你只需要背好一以下幾點就可以了
當主執行緒同步程式碼執行完畢後才會進入事件迴圈
事件迴圈總共分六個階段,並且每個階段都包括哪些回撥需要記清楚。
事件迴圈中會先執行微任務再執行宏任務。
微任務會穿插在這六個階段之間執行,每進入到下個階段前會清空當前的微任務佇列。
微任務中process.nextTick
的優先順序最高,會優先執行。
更多node相關知識,請存取:!
以上就是深入瞭解Node事件迴圈(EventLoop)機制的詳細內容,更多請關注TW511.COM其它相關文章!