Nodejs 解讀event loop的事件處理機(jī)制

摘要:

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)作的扑庞?

eventloop.jpg

上述的五個階段都是按照先進(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ù):
    1. 當(dāng)timers 的定時器到時后胶哲,執(zhí)行定時器(setTimeout 和 setInternal)的回調(diào)函數(shù)畔塔。
    2. 執(zhí)行poll 隊列里面的I/O 隊列。
poll phrase.png

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”事件公罕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末器紧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子楼眷,更是在濱河造成了極大的恐慌铲汪,老刑警劉巖熊尉,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掌腰,居然都是意外死亡狰住,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門齿梁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來催植,“玉大人,你說我怎么就攤上這事勺择〈茨希” “怎么了省核?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵气忠,是天一觀的道長笔刹。 經(jīng)常有香客問我,道長萌壳,這世上最難降的妖魔是什么日月? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任尺借,我火速辦了婚禮精拟,結(jié)果婚禮上蜂绎,老公的妹妹穿的比我還像新娘师枣。我一直安慰自己,他們只是感情好践美,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敛滋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爹脾。 梳的紋絲不亂的頭發(fā)上箕昭,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天落竹,我揣著相機(jī)與錄音货抄,去河邊找鬼蟹地。 笑死怪与,一個胖子當(dāng)著我的面吹牛分别,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沼填,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荚虚!你這毒婦竟也來了曲管?” 一聲冷哼從身側(cè)響起却邓,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎院水,沒想到半個月后简十,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡螟蝙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胰默。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡漓踢,死狀恐怖牵署,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奴迅,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站扁耐,受9級特大地震影響块仆,放射性物質(zhì)發(fā)生泄漏酿矢。R本人自食惡果不足惜榨乎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望策肝。 院中可真熱鬧肛捍,春花似錦、人聲如沸之众。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棺禾。三九已至缀蹄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缺前。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工蛀醉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人衅码。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓拯刁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逝段。 傳聞我的和親對象是個殘疾皇子垛玻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容