摘要:
1. nodejs 為什么要存在一個event loop的事件處理機(jī)制茅逮?
2. event loop的事件處理機(jī)制如何運(yùn)作的锯七?
3. 從event loop機(jī)制的角度上區(qū)分setImmediate()與setTimeout()
4. 從event loop機(jī)制的角度上區(qū)分process.nextTick()與setImmediate()
noted:內(nèi)容來源對nodejs 官方文檔學(xué)習(xí)總結(jié)
1. nodejs 為什么要存在一個event loop的事件處理機(jī)制挤聘?
nodejs 具有事件驅(qū)動和非阻塞但線程的特點(diǎn)舷手,使相關(guān)應(yīng)用變得比較輕量和高效着倾。當(dāng)應(yīng)用程序需要相關(guān)I/O操作時拾酝,線程并不會阻塞,而是把I/O操作移交給底層類庫(如:libuv)卡者。此時nodejs線程會去處理其他的任務(wù)蒿囤,當(dāng)?shù)讓訋焯幚硗晗嚓P(guān)的I/O操作后,會將主動權(quán)再次交還給nodejs線程虎眨。因此event loop的作用就是起到調(diào)度線程的作用蟋软,如當(dāng)?shù)讓宇悗焯幚鞩/O操作后調(diào)度nodejs單線程處理后續(xù)的工作。也就是說當(dāng)nodejs 程序啟動的時候嗽桩,它會開啟一個event loop以實現(xiàn)異步的api調(diào)度岳守、schedule timers 、回調(diào)process.nextTick()碌冶。
從上也可以看出nodejs 雖說是單線程湿痢,但是在底層類庫處理異步操作的時候仍然是多線程。
2. event loop的事件處理機(jī)制如果運(yùn)作的扑庞?
上述的五個階段都是按照先進(jìn)先出的規(guī)則執(zhí)行回調(diào)函數(shù)譬重。按順序執(zhí)行每個階段的回調(diào)函數(shù)隊列,直至隊列為空或是該階段執(zhí)行的回調(diào)函數(shù)達(dá)到該階段所允許一次執(zhí)行回調(diào)函數(shù)的最大限制后罐氨,才會將操作權(quán)移交給下一階段臀规。
每個階段的簡單概要:
- timers: 執(zhí)行setTimeout() 和 setInterval() 預(yù)先設(shè)定的回調(diào)函數(shù)。
- I/O callbacks: 大部分執(zhí)行都是timers 階段或是setImmediate() 預(yù)先設(shè)定的并且出現(xiàn)異常的回調(diào)函數(shù)事件栅隐。
- idle, prepare: nodejs 內(nèi)部函數(shù)調(diào)用塔嬉。
- poll: 搜尋I/O事件玩徊,nodejs進(jìn)程在這個階段會選擇在該階段適當(dāng)?shù)淖枞欢螘r間。
- check: setImmediate() 函數(shù)會在這個階段執(zhí)行谨究。
- close callbacks: 執(zhí)行一些諸如關(guān)閉事件的回調(diào)函數(shù)恩袱,如socket.on('close', ...) 。
每個階段的詳細(xì)內(nèi)容:
- poll
該階段主要是兩個任務(wù):- 當(dāng)timers 的定時器到時后胶哲,執(zhí)行定時器(setTimeout 和 setInternal)的回調(diào)函數(shù)畔塔。
- 執(zhí)行poll 隊列里面的I/O 隊列。
Noted:在poll phrase一旦event loop中的poll queue隊列為空鸯屿,poll 就會去timers 查看有沒有到期的定時期需要執(zhí)行澈吨。如果有,就會返回timer執(zhí)行相應(yīng)的回調(diào)函數(shù)碾盟。
- timers
指定線程執(zhí)行定時器(setTimeout和 setInterval)的回調(diào)函數(shù)棚辽,但是大多數(shù)的時候定時器的回調(diào)函數(shù)執(zhí)行的時間要遠(yuǎn)大于定時器設(shè)定的時間。因為必須要等poll phrase中的poll queue隊列為空時冰肴,poll才會去查看timer中有沒有到期的定時器然后去執(zhí)行定時器中的回調(diào)函數(shù)屈藐。
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/Users/spursy/Develop/TFDemo/activateTF.txt', callback);
}
const timeoutScheduled = Date.now();
setTimeout(function() {
const delay = Date.now() - timeoutScheduled;
console.log(delay + 'ms have passed since I was scheduled');
}, 100);
// do someAsyncOperation which takes 94 ms to complete
someAsyncOperation(function() {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
上面的函數(shù)執(zhí)行的結(jié)果是:
104ms have passed since I was scheduled
定時器加入到timer中,定時的時間設(shè)置為100秒熙尉。poll中執(zhí)行的I/O操作联逻,由于讀取相應(yīng)目錄下的文件要耗費(fèi)一些時間,poll將會阻塞在這里循環(huán)相應(yīng)的回調(diào)函數(shù)检痰,大約在94秒時相應(yīng)的I/O操作執(zhí)行完畢包归,對應(yīng)的回調(diào)函數(shù)又耗費(fèi)了10秒鐘。這時poll queue為空铅歼,此時poll會去timer查看有沒有到期的定時器公壤。發(fā)現(xiàn)存在一個已經(jīng)超時近4秒的定時器然后就執(zhí)行定時器對應(yīng)的回調(diào)函數(shù),這樣就是定時器執(zhí)行了將近104秒鐘時間的原因椎椰。
- I/O callbacks
該階段執(zhí)行一些諸如TCP的errors回調(diào)函數(shù)厦幅。 - check
如果poll中已沒有排隊的隊列,并且存在setImmediate() 立即執(zhí)行的回調(diào)函數(shù)慨飘,這是event loop不會在poll階段阻塞等待相應(yīng)的I/O事件确憨,而是直接去check階段執(zhí)行setImmediate() 函數(shù)。 - close callback
該階段執(zhí)行close的事件函數(shù)瓤的。
3. 從event loop機(jī)制的角度上區(qū)分setImmediate()與setTimeout()
從Issue 2中poll和check階段的邏輯休弃,我們可以看出setImmediate和setTimeout、setInterval都是在poll 階段執(zhí)行完當(dāng)前的I/O隊列中相應(yīng)的回調(diào)函數(shù)后觸發(fā)的圈膏。但是這兩個函數(shù)卻是由不同的路徑觸發(fā)的塔猾。
setImmediate函數(shù),是在當(dāng)前的poll queue對列執(zhí)行后為空或是執(zhí)行的數(shù)目達(dá)到上限后稽坤,event loop直接調(diào)入check階段執(zhí)行setImmediate函數(shù)桥帆。
setTimeout医增、setInterval則是在當(dāng)前的poll queue對列執(zhí)行后為空或是執(zhí)行的數(shù)目達(dá)到上限后,event loop去timers檢查是否存在已經(jīng)到期的定時器老虫,如果存在直接執(zhí)行相應(yīng)的回調(diào)函數(shù)。
如果程序中既有setTimeout和setImmediate茫多,兩者的執(zhí)行順序是什么祈匙?
// timeout_vs_immediate.js
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
上面的程序執(zhí)行的結(jié)果并不是唯一的,有時immediate在前天揖,有時timeout在qian夺欲。主要是由于他們運(yùn)行的當(dāng)前上下文環(huán)境中存在其他的程序影響了他們執(zhí)行順序。
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
上面的程序把setImmediate和setTimeout放到了I/O循環(huán)中今膊,此時他們的執(zhí)行順序永遠(yuǎn)都是immediate在前些阅,timeout在后。
4. 從event loop機(jī)制的角度上區(qū)分process.nextTick()與setImmediate()
1. process.nextTick()函數(shù)
- 盡管process.nextTick()也是一個異步的函數(shù)斑唬,但是它并沒有出現(xiàn)在上面event loop的結(jié)構(gòu)圖中市埋。不管當(dāng)前正在event loop的哪個階段,在當(dāng)前階段執(zhí)行完畢后恕刘,跳入下個階段前的瞬間執(zhí)行process.nextTick()函數(shù)缤谎。
- 由于process.nextTick()函數(shù)的特性,很可能出現(xiàn)一種惡劣的情形:在event loop進(jìn)入poll前調(diào)用該函數(shù)褐着,就會阻止程序進(jìn)入poll階段allows you to "starve" your I/O by making recursive process.nextTick() calls坷澡。
- 但是也正是nodejs的一個設(shè)計哲學(xué):每個函數(shù)都可以是異步的,即使它不必這樣做含蓉。例如下面程序片段频敛,如果不對內(nèi)部函數(shù)作異步處理就可能出現(xiàn)異常。
let bar;
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }
// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
});
bar = 1;
由于someAsyncApiCall函數(shù)在執(zhí)行時馅扣,內(nèi)部函數(shù)是同步的斟赚,這是變量bar還沒有被賦值。如果改為下面的就會這個異常岂嗓。
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
2. 兩者的比較
- process.nextTick() 函數(shù)是在任何階段執(zhí)行結(jié)束的時刻
- setImmediate() 函數(shù)是在poll階段后進(jìn)去check階段事執(zhí)行
3. process.nextTick() 函數(shù)的應(yīng)用
- 允許線程在進(jìn)入event loop下一個階段前做一些關(guān)于處理異常汁展、清理一些無用或無關(guān)的資源。l例如下面:
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}
- 在進(jìn)入下個event loop階段前厌殉,并且回調(diào)函數(shù)還沒有釋放回調(diào)權(quán)限時執(zhí)行一些相關(guān)操作食绿。如下代碼:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(function() {
this.emit('event');
}.bind(this));
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
console.log('an event occurred!');
});
在MyEmitter構(gòu)造函數(shù)實例化前注冊“event”事件,這樣就可以保證實例化后的函數(shù)可以監(jiān)聽“event”事件公罕。