nodejs中架構(gòu)如下圖所示,通過v8引擎來執(zhí)行js代碼,通過中間層 libuv 來讀寫文件系統(tǒng)、網(wǎng)絡(luò)等做一些操作。
nodejs中提供阻塞和非阻塞的調(diào)用方式,比如fs模塊中讀取文件,可以根據(jù)需要使用 readFile(異步) 或者 readFileSync(同步)。
如果使用同步的編程方式,那么后續(xù)代碼的執(zhí)行需要等到此次執(zhí)行結(jié)束,程序的運(yùn)行會(huì)被“阻塞”。也可以使用異步的編程方式,后續(xù)代碼的執(zhí)行無需等待此次的執(zhí)行結(jié)束,不會(huì)“阻塞”程序的運(yùn)行,但它同樣也會(huì)存在一個(gè)問題,那就是非阻塞式調(diào)用需要不斷的輪詢獲取異步調(diào)用的執(zhí)行結(jié)果。
libuv中的io操作就是使用的”非阻塞式調(diào)用“,但它如果不斷輪詢獲取結(jié)果這個(gè)過程對(duì)系統(tǒng)有一定的性能影響,為了將這個(gè)影響降低,libuv中將不斷輪詢的這個(gè)過程放到“線程池”當(dāng)中,當(dāng)輪詢到結(jié)果時(shí),將對(duì)應(yīng)的回調(diào)和獲取的結(jié)果放置到 event loop(事件循環(huán))的某一個(gè)隊(duì)列中,事件循環(huán)再來進(jìn)行下一步的操作,通過javascript來執(zhí)行回調(diào)。
nodejs中的事件循環(huán)要比javascript的事件循環(huán)更為復(fù)雜一些,一次循環(huán)分為以下幾個(gè)階段
* 定時(shí)器(timer): 在這個(gè)階段執(zhí)行 setTimeout、setInterval的回調(diào)函數(shù)
* 待定回調(diào)(pending callbacks):某些系統(tǒng)操作(如 TCP 錯(cuò)誤類型)執(zhí)行回調(diào)
* idle, prepare:僅系統(tǒng)內(nèi)部使用。
* 輪詢(poll):檢索新的 I/O 事件; 執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù),那些由計(jì)時(shí)器和 setImmediate() 調(diào)度的之外)
* 檢測(cè)(check):setImmediate() 回調(diào)函數(shù)在這里執(zhí)行。
* 關(guān)閉的回調(diào)函數(shù)(close callbacks):一些關(guān)閉的回調(diào)函數(shù),如:socket.on('close', ...)。
nodejs中也和javascript中一樣存在著微任務(wù)(micro-task)、宏任務(wù)(macro-task),兩個(gè)任務(wù)中執(zhí)行的內(nèi)容也有一部分的相似性
微任務(wù):promise的then函數(shù)的回調(diào)、queueMicrotask、process.nextTick
宏任務(wù): setTimeout、setInterval、io事件、setImmediate、close事件
執(zhí)行順序也和javascript中一致,先執(zhí)行主線程的任務(wù),然后接著執(zhí)微任務(wù),微任務(wù)執(zhí)行完成再執(zhí)行宏任務(wù),具體的執(zhí)行順序如下。
微任務(wù)隊(duì)列
next tick queue:process.nextTick
other tick queue:promise的then函數(shù)、queueMicrotask
宏任務(wù)隊(duì)列
timer queue: setTimeout、setInterval
poll queue: io事件
check queue: setImmediate
close queue: close事件
了解完nodejs中事件循環(huán)的執(zhí)行順序之后,一起來看看下面這道面試題
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('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
首先聲明了 async1 和 async2函數(shù),只有調(diào)用才會(huì)被放入調(diào)用棧中,所以此時(shí)不會(huì)執(zhí)行,往下執(zhí)行輸出 "script start"。
繼續(xù)向下執(zhí)行,將 "setTimeout0" 放入 宏任務(wù)的 timer queue 中,繼續(xù)執(zhí)行,"setTimeout2" 需要延遲300ms執(zhí)行,不放入 timer queue中,接著把 "setImmediate" 放入 check queue 中,把 "nextTick1" 放入 next tick queue 中。
此時(shí)執(zhí)行函數(shù) async1,輸出 "async1 start",在函數(shù) async1 中執(zhí)行 async2,輸出 "async2",把 "async1 end" 放入 other queue當(dāng)中,此時(shí)函數(shù) async1執(zhí)行完成,繼續(xù)向下執(zhí)行,將"nextTick2"放入next tick queue中,排在"nextTick1"后面,繼續(xù)輸出 "promise1",將"promise3"放在other queue中,排在"async1 end"后面,輸出"promise2",最后輸出"script end",此時(shí)主線程的內(nèi)容都執(zhí)行完成。
來到微任務(wù)隊(duì)列,微任務(wù)隊(duì)列中先執(zhí)行 next tick queue 中的內(nèi)容,依次輸出 "nectTick1"、"nectTick2",再執(zhí)行 other queue 中的內(nèi)容,依次輸出"async1 end"、"promise3"。
最后執(zhí)行宏任務(wù)隊(duì)列中的任務(wù),先執(zhí)行 timer queue,輸出 "setTimeout0",這里沒有io事件(poll),往下執(zhí)行check quue,輸出 setImmediate,沒有關(guān)閉回調(diào)函數(shù)階段( close callbasks),一次事件循環(huán)結(jié)束,來到第二次、第三次事件循環(huán),此時(shí)300ms后,"setTimeout2" 被加入到宏任務(wù)隊(duì)列中的 timer queue,事件循環(huán)中沒有其他的隊(duì)列,直接輸出 "setTimeout2",事件循環(huán)結(jié)束。
簡(jiǎn)單圖示如下
再來看另一道面試題
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
按照宏任務(wù)隊(duì)列中各任務(wù)的執(zhí)行順序,setTimeout屬于timer queue,setImmediate屬于check queue,按理說會(huì)先輸出setTimeout,但實(shí)際情況會(huì)是什么樣的呢,我們看一下以下輸出情況
為什么會(huì)這樣的情況呢,有時(shí)候是setTimeout先輸出,有時(shí)候是setImmediate先輸出,原因在于,setTimeout的回調(diào)函數(shù)雖然是延遲0毫秒執(zhí)行,但是 setTimeout的準(zhǔn)備時(shí)間要長于 event loop 的啟動(dòng)時(shí)間,當(dāng)event loop 開始第一次循環(huán)的時(shí)候,setTimeout 還沒有被放入 timer queue 之中,所以 event loop 先執(zhí)行 check queue 中的 setImmediate,等待第二次循環(huán)的時(shí)候,timer queue 中才有setTimeout,此時(shí)就會(huì)出現(xiàn) setImmediate 先輸出的情況。
nodejs中的事件循環(huán)機(jī)制有一部分和javascript中事件循環(huán)是一致的,如果對(duì)javascript事件循環(huán)機(jī)制還不太熟悉,可以看看這一篇文章,javascript事件循環(huán)機(jī)制及面試題詳解