并發(fā)模型
常見的并發(fā)模型是并行工作者模型,任務(wù)分配給多個(gè)工作者,每個(gè)工作者完成整個(gè)任務(wù),常說的 C 語言的多線程就是這種模型,它的工作模式如下圖。

而 Node.js 用的并發(fā)模型是事件驅(qū)動(dòng)模型,工作者對出現(xiàn)的事件做出反應(yīng),自身也能產(chǎn)生事件,它的工作模式如下圖。

單線程、同異步
常說的 JavaScript 的單線程指的是用戶代碼執(zhí)行上的單線程,即同一時(shí)間只能執(zhí)行一段代碼,這與 ?C 語言同一時(shí)間可以并行執(zhí)行多段代碼形成鮮明的對比。
所以 Node.js 的執(zhí)行可以簡單地分成兩個(gè)階段:
- 初始化代碼執(zhí)行
- 事件循環(huán)
初始化代碼執(zhí)行里,執(zhí)行所有的同步操作代碼。所謂同步操作,就是永遠(yuǎn)一步步執(zhí)行、沒有結(jié)果不繼續(xù)執(zhí)行后面代碼的操作。對應(yīng)的異步操作是不等待結(jié)果就繼續(xù)執(zhí)行后面代碼的操作。一般異步操作都帶有一個(gè)回調(diào)函數(shù),而回調(diào)函數(shù)里的操作不包括在上面說的「后面代碼」里,而是異步操作完成以后希望要執(zhí)行的操作,它們需要排隊(duì)等待被執(zhí)行。
異步操作的回調(diào)函數(shù)排隊(duì)等待被執(zhí)行就算在事件循環(huán)這一階段。在執(zhí)行完所有同步代碼以后,Node.js 查看回調(diào)隊(duì)列里有沒有任務(wù),有的話就執(zhí)行,沒有的話就等待異步操作完成,因?yàn)閹в谢卣{(diào)任務(wù)的異步操作完成時(shí)會(huì)將回調(diào)任務(wù)入隊(duì)到回調(diào)隊(duì)列,這樣就有任務(wù)可以執(zhí)行了。所以可以很自然地推理出,如果回調(diào)隊(duì)列為空且沒有需要等待完成的異步操作,這個(gè) Node.js 進(jìn)程就結(jié)束了。事實(shí)也是如此。
由上也可以知道,所有的用戶代碼最終都是在同一線程也就是主線程上面順序執(zhí)行的。而回調(diào)函數(shù)就是執(zhí)行順序不是按聲明順序來執(zhí)行而是要經(jīng)過 Node.js 的事件循環(huán)來安排執(zhí)行的用戶代碼。
Node.js 異步操作的執(zhí)行
我們知道 Node.js 的所有異步操作都是由 Libuv 來負(fù)責(zé)的。Libuv 將可以給系統(tǒng)內(nèi)核來執(zhí)行的異步操作都交給了系統(tǒng)內(nèi)核來執(zhí)行,只有當(dāng)系統(tǒng)不能執(zhí)行這個(gè)操作的時(shí)候才會(huì)用自己的線程池來執(zhí)行這個(gè)異步操作。下圖列出了一些異步操作一般由誰來執(zhí)行:(圖來自:Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM)

事件循環(huán)順序

如上圖,每一個(gè)方框代表一個(gè)事件循環(huán)階段,每一階段都有自己的先進(jìn)先出的任務(wù)隊(duì)列。從用戶代碼入口開始,執(zhí)行完所有同步代碼后進(jìn)入事件循環(huán),在事件循環(huán)里的每一個(gè)階段都查看該階段的任務(wù)隊(duì)列是否為空,如果不為空則嘗試同步執(zhí)行(以先進(jìn)先出順序一個(gè)一個(gè)執(zhí)行)所有隊(duì)列里的任務(wù)直到隊(duì)列為空。這里輪詢事件階段的任務(wù)執(zhí)行有最大次數(shù)限制。之后會(huì)細(xì)講。
實(shí)際上事件循環(huán)里包含的階段比圖上列出的多,但是我們應(yīng)該關(guān)心的都在圖上列出來了。
setTimeout、setInterval
由 setTimeout 、setInterval 調(diào)度的回調(diào)任務(wù)在這里排隊(duì)執(zhí)行。
I/O
像是由網(wǎng)絡(luò)、磁盤數(shù)據(jù)、子進(jìn)程等 I/O 類調(diào)度的回調(diào)任務(wù)在這里排隊(duì)執(zhí)行。
輪詢事件
查看是否有新的 I/O 事件,為下個(gè)輪詢的 I/O 階段提供任務(wù)。
如果所有隊(duì)列為空,這里阻塞主線程進(jìn)入沉睡,直到發(fā)生以下事件之一:
- 有新的 I/O 事件發(fā)生
- 有子線程完成任務(wù)
- 有定時(shí)器達(dá)到閾值
也就是說,上面的事件的發(fā)生都會(huì)進(jìn)入這階段的事件任務(wù)隊(duì)列,當(dāng)事件隊(duì)列不為空時(shí)就執(zhí)行到空或達(dá)到最大次數(shù)限制(因?yàn)檫@階段在處理事件的時(shí)候可以產(chǎn)生新事件入隊(duì)而導(dǎo)致隊(duì)列一直不為空從而阻塞事件循環(huán),所以有最大次數(shù)限制)。
setImmediate
通過 setImmediate 設(shè)置的回調(diào)在這里排隊(duì)執(zhí)行。
'close' 事件
on('close') 事件調(diào)用的回調(diào)在這里排隊(duì)執(zhí)行。
setTimeout/setImmediate
對于在非 I/O 回調(diào)里的 setTimeout 和 setImmediate 來說,執(zhí)行的先后順序無法確定,而在 I/O 回調(diào)里 setImmediate 總是比 setTimeout 先執(zhí)行。
如在主模塊里的這段代碼:
setTimeout(() => {
console.log('in setTimeout')
}, 0)
setImmediate(() => {
console.log('in setImmediate')
})
運(yùn)行結(jié)果可能是:
in setTimeout
in setImmediate
也可能是:
in setImmediate
in setTimeout
而下面這段代碼:
const fs = require('fs');
fs.readFile(filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
setImmediate 在 setTimeout 之前。
特殊的 process.nextTick() 和 Promise.resolve()
process.nextTick() 和 Promise.resolve() 不在上面的循環(huán)圖里的階段里面,它們也有一個(gè)自己的任務(wù)隊(duì)列,在每個(gè)階段結(jié)束的時(shí)候都會(huì)查看這個(gè)隊(duì)列是否為空,如果不為空就一個(gè)個(gè)執(zhí)行里面所有的任務(wù)直到隊(duì)列為空。
執(zhí)行邏輯大概如下圖:

顯然在遞歸調(diào)用 process.nextTick() 或 Promise.resolve() 的時(shí)候任務(wù)隊(duì)列一直不為空則會(huì)引起阻塞,但是它們的存在又確實(shí)是必要的:
- 用戶要在事件循環(huán)繼續(xù)之前處理錯(cuò)誤、清理資源
- 在當(dāng)前執(zhí)行棧之后且在事件循環(huán)之前需要執(zhí)行一個(gè)回調(diào)
官方文檔舉了這樣一個(gè)例子:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event'); // 這里對 `event` 事件的監(jiān)聽還沒運(yùn)行到,則這個(gè) emit 不能觸發(fā)對應(yīng)的回調(diào)
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
用了 process.nextTick() 后:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// 先執(zhí)行了所有同步代碼然后才執(zhí)行 process.nextTick 的回調(diào)即 emit 一個(gè) event 事件
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
參考:
并發(fā)模型
The Node.js Event Loop, Timers, and process.nextTick()
What you should know to really understand the Node.js Event Loop
Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM
Understanding the Node.js Event Loop