您好,登錄后才能下訂單哦!
這篇文章主要介紹“Node事件循環機制是什么”,在日常操作中,相信很多人在Node事件循環機制是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Node事件循環機制是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
雖然js
可以在瀏覽器中執行又可以在node
中執行,但是它們的事件循環機制并不是一樣的。并且有很大的區別。
在說Node
事件循環機制之前,我們先來討論兩個問題
學習事件循環可以讓開發者明白JavaScript
的運行機制是怎么樣的。
事件循環機制用于管理異步API的回調函數什么時候回到主線程中執行。
Node.js采用的是異步IO模型。同步API在主線程中執行,異步API在底層的C++維護的線程中執行,異步API的回調函數也會在主線程中執行。【相關教程推薦:nodejs視頻教程、編程教學】
在Javascript應用運行時,眾多異步API的回調函數什么時候能回到主線程中調用呢?這就是事件環環機制做的事情,管理異步API的回調函數什么時候回到主線程中執行。
在Node
中的事件循環分為六個階段。
在事件循環中的每個階段都有一個隊列,存儲要執行的回調函數,事件循環機制會按照先進先出的方式執行他們直到隊列為空。
這六個階段都存儲著異步回調函數,所以還是遵循先執行主線程同步代碼,當同步代碼執行完后再來輪詢這六個階段。
接下來,我們來詳細看看這六個階段里面存儲的都是什么
Timers
:用于存儲定時器的回調函數(setlnterval,setTimeout)。
Pendingcallbacks
:執行與操作系統相關的回調函數,比如啟動服務器端應用時監聽端口操作的回調函數就在這里調用。
idle,prepare
:系統內部使用。(這個我們程序員不用管)
Poll
:存儲1/O操作的回調函數隊列,比如文件讀寫操作的回調函數。
在這個階段需要特別注意,如果事件隊列中有回調函數,則執行它們直到清空隊列 ,否則事件循環將在此階段停留一段時間以等待新的回調函數進入。
但是對于這個等待并不是一定的,而是取決于以下兩個條件:
如果setlmmediate隊列(check階段)中存在要執行的調函數。這種情況就不會等待。
timers隊列中存在要執行的回調函數,在這種情況下也不會等待。事件循環將移至check階段,然后移至Closingcallbacks階段,并最終從timers階段進入下一次循環。
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事件循環機制是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。