主線程從"任務(wù)隊列"中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。下面本篇文章就來帶大家掌握Node.js中的eventloop,希望對大家有所幫助!
(相關(guān)資料圖)
雖然js可以在瀏覽器中執(zhí)行又可以在node中執(zhí)行,但是它們的事件循環(huán)機制并不是一樣的。并且有很大的區(qū)別。
在說Node事件循環(huán)機制之前,我們先來討論兩個問題
學習事件循環(huán)可以讓開發(fā)者明白JavaScript的運行機制是怎么樣的。
事件循環(huán)機制用于管理異步API的回調(diào)函數(shù)什么時候回到主線程中執(zhí)行。
Node.js采用的是異步IO模型。同步API在主線程中執(zhí)行,異步API在底層的C++維護的線程中執(zhí)行,異步API的回調(diào)函數(shù)也會在主線程中執(zhí)行?!鞠嚓P(guān)教程推薦:nodejs視頻教程、編程教學】
在Javascript應(yīng)用運行時,眾多異步API的回調(diào)函數(shù)什么時候能回到主線程中調(diào)用呢?這就是事件環(huán)環(huán)機制做的事情,管理異步API的回調(diào)函數(shù)什么時候回到主線程中執(zhí)行。
在Node中的事件循環(huán)分為六個階段。
在事件循環(huán)中的每個階段都有一個隊列,存儲要執(zhí)行的回調(diào)函數(shù),事件循環(huán)機制會按照先進先出的方式執(zhí)行他們直到隊列為空。
這六個階段都存儲著異步回調(diào)函數(shù),所以還是遵循先執(zhí)行主線程同步代碼,當同步代碼執(zhí)行完后再來輪詢這六個階段。
接下來,我們來詳細看看這六個階段里面存儲的都是什么
Timers:用于存儲定時器的回調(diào)函數(shù)(setlnterval,setTimeout)。
Pendingcallbacks:執(zhí)行與操作系統(tǒng)相關(guān)的回調(diào)函數(shù),比如啟動服務(wù)器端應(yīng)用時監(jiān)聽端口操作的回調(diào)函數(shù)就在這里調(diào)用。
idle,prepare:系統(tǒng)內(nèi)部使用。(這個我們程序員不用管)
Poll:存儲1/O操作的回調(diào)函數(shù)隊列,比如文件讀寫操作的回調(diào)函數(shù)。
在這個階段需要特別注意,如果事件隊列中有回調(diào)函數(shù),則執(zhí)行它們直到清空隊列,否則事件循環(huán)將在此階段停留一段時間以等待新的回調(diào)函數(shù)進入。
但是對于這個等待并不是一定的,而是取決于以下兩個條件:
如果setlmmediate隊列(check階段)中存在要執(zhí)行的調(diào)函數(shù)。這種情況就不會等待。timers隊列中存在要執(zhí)行的回調(diào)函數(shù),在這種情況下也不會等待。事件循環(huán)將移至check階段,然后移至Closingcallbacks階段,并最終從timers階段進入下一次循環(huán)。Check:存儲setlmmediate的回調(diào)函數(shù)。
Closingcallbacks:執(zhí)行與關(guān)閉事件相關(guān)的回調(diào),例如關(guān)閉數(shù)據(jù)庫連接的回調(diào)函數(shù)等。
跟瀏覽器中的js一樣,node中的異步代碼也分為宏任務(wù)和微任務(wù),只是它們之間的執(zhí)行順序有所區(qū)別。
我們再來看看Node中都有哪些宏任務(wù)和微任務(wù)
setlnterval
setimeout
setlmmediate
I/O
Promise.then
Promise.catch
Promise.finally
process.nextTick
在node中,對于微任務(wù)和宏任務(wù)的執(zhí)行順序到底是怎樣的呢?
在node中,微任務(wù)的回調(diào)函數(shù)被放置在微任務(wù)隊列中,宏任務(wù)的回調(diào)函數(shù)被放置在宏任務(wù)隊列中。
微任務(wù)優(yōu)先級高于宏任務(wù)。當微任務(wù)事件隊列中存在可以執(zhí)行的回調(diào)函數(shù)時,事件循環(huán)在執(zhí)行完當前階段的回調(diào)函數(shù)后會暫停進入事件循環(huán)的下一個階段,而會立即進入微任務(wù)的事件隊列中開始執(zhí)行回調(diào)函數(shù),當微任務(wù)隊列中的回調(diào)函數(shù)執(zhí)行完成后,事件循環(huán)才會進入到下一個段開始執(zhí)行回調(diào)函數(shù)。
對于微任務(wù)我們還有個點需要特別注意。那就是雖然nextTick同屬于微任務(wù),但是它的優(yōu)先級是高于其它微任務(wù),在執(zhí)行微任務(wù)時,只有nextlick中的所有回調(diào)函數(shù)執(zhí)行完成后才會開始執(zhí)行其它微任務(wù)。
總的來說就是當主線程同步代碼執(zhí)行完畢后會優(yōu)先清空微任務(wù)(如果微任務(wù)繼續(xù)產(chǎn)生微任務(wù)則會再次清空),然后再到下個事件循環(huán)階段。并且微任務(wù)的執(zhí)行是穿插在事件循環(huán)六個階段中間的,也就是每次事件循環(huán)進入下個階段前會判斷微任務(wù)隊列是否為空,為空才會進入下個階段,否則先清空微任務(wù)隊列。
下面我們用代碼實操來驗證前面所說的。
在Node應(yīng)用程序啟動后,并不會立即進入事件循環(huán),而是先執(zhí)行同步代碼,從上到下開始執(zhí)行,同步API立即執(zhí)行,異步API交給C++維護的線程執(zhí)行,異步API的回調(diào)函數(shù)被注冊到對應(yīng)的事件隊列中。當所有同步代碼執(zhí)行完成后,才會進入事件循環(huán)。
console.log("start");setTimeout(() => { console.log("setTimeout 1");});setTimeout(() => { console.log("setTimeout 2");});console.log("end");我們來看執(zhí)行結(jié)果
可以看到,先執(zhí)行同步代碼,然后才會進入事件循環(huán)執(zhí)行異步代碼,在timers階段執(zhí)行兩個setTimeout回調(diào)。
我們知道setTimeout是在timers階段執(zhí)行,setImmediate是在check階段執(zhí)行。并且事件循環(huán)是從timers階段開始的。所以會先執(zhí)行setTimeout再執(zhí)行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");執(zhí)行上面的代碼,輸出如下
先執(zhí)行setTimeout再執(zhí)行setImmediate
接下來我們來改造下上面的代碼,把延遲器去掉,看看會輸出什么
setTimeout(() => { console.log("setTimeout");});setImmediate(() => { console.log("setImmediate");});我們運行了七次,可以看到其中有兩次是先運行的setImmediate
怎么回事呢?不是先timers階段再到check階段嗎?怎么會變呢?
其實這就得看進入事件循環(huán)的時候,異步回調(diào)有沒有完全準備好了。對于最開始的例子,因為有2000毫秒的延遲,所以進入事件循環(huán)的時候,setTimeout回調(diào)是一定準備好了的。所以執(zhí)行順序不會變。但是對于這個例子,因為主線程沒有同步代碼需要執(zhí)行,所以一開始就進入事件循環(huán),但是在進入事件循環(huán)的時候,setTimeout的回調(diào)并不是一定完全準備好的,所以就會有先到check階段執(zhí)行setImmediate回調(diào)函數(shù),再到下一次事件循環(huán)的timers階段來執(zhí)行setTimeout的回調(diào)。
那在什么情況下同樣的延遲時間,setImmediate回調(diào)函數(shù)一定會優(yōu)先于setTimeout的回調(diào)呢?
其實很簡單,只要將這兩者放到timers階段和check階段之間的Pendingcallbacks、idle,prepare、poll階段中任意一個階段就可以了。因為這些階段完執(zhí)行完是一定會先到check再到timers階段的。
我們以poll階段為例,將這兩者寫在IO操作中。
const fs = require("fs");fs.readFile("./fstest.js", "utf8", (err, data) => { setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); });});我們也來執(zhí)行七次,可以看到,每次都是setImmediate先執(zhí)行。
所以總的來說,同樣的延遲時間,setTimeout并不是百分百先于setImmediate執(zhí)行。
主線程同步代碼執(zhí)行完畢后,會先執(zhí)行微任務(wù)再執(zhí)行宏任務(wù)。
我們來看下面的例子
console.log("start");setTimeout(() => { console.log("setTimeout");});setImmediate(() => { console.log("setImmediate");});Promise.resolve().then(() => { console.log("Promise.resolve");});console.log("end");我們運行一下看結(jié)果,可以看到它是先執(zhí)行了微任務(wù)然后再執(zhí)行宏任務(wù)
在微任務(wù)中nextTick的優(yōu)先級是最高的。
我們來看下面的例子
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后面,它也是先執(zhí)行的。
怎么理解這個穿插呢?其實就是在事件循環(huán)的六個階段每個階段執(zhí)行完后會清空微任務(wù)隊列。
我們來看例子,我們建立了timers、check、poll三個階段,并且每個階段都產(chǎn)生了微任務(wù)。
// 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"); });});// 微任務(wù)Promise.resolve().then(() => { console.log("Promise.resolve");});// 微任務(wù)process.nextTick(() => { console.log("process.nextTick"); Promise.resolve().then(() => { console.log("nextTick Promise.resolve"); });});我們來執(zhí)行上面的代碼
可以看到,先執(zhí)行微任務(wù),再執(zhí)行宏任務(wù)。先process.nextTick -> Promise.resolve。并且如果微任務(wù)繼續(xù)產(chǎn)生微任務(wù)則會再次清空,所以就又輸出了nextTick Promise.resolve。
接下來到timer階段,輸出setTimeout,并且產(chǎn)生了一個微任務(wù),再進入到下個階段前需要清空微任務(wù)隊列,所以繼續(xù)輸出setTimeout Promise.resolve。
接下來到check階段,輸出setImmediate,并且產(chǎn)生了一個微任務(wù),再進入到下個階段前需要清空微任務(wù)隊列,所以繼續(xù)輸出setImmediate Promise.resolve。
這也就印證了微任務(wù)會穿插在各個階段之間運行。
所以對于Node中的事件循環(huán)你只需要背好一以下幾點就可以了
當主線程同步代碼執(zhí)行完畢后才會進入事件循環(huán)
事件循環(huán)總共分六個階段,并且每個階段都包括哪些回調(diào)需要記清楚。
事件循環(huán)中會先執(zhí)行微任務(wù)再執(zhí)行宏任務(wù)。
微任務(wù)會穿插在這六個階段之間執(zhí)行,每進入到下個階段前會清空當前的微任務(wù)隊列。
微任務(wù)中process.nextTick的優(yōu)先級最高,會優(yōu)先執(zhí)行。
更多node相關(guān)知識,請訪問:nodejs 教程!
以上就是深入了解Node事件循環(huán)(EventLoop)機制的詳細內(nèi)容,更多請關(guān)注php中文網(wǎng)其它相關(guān)文章!
關(guān)鍵詞: