Node.js事件循環(huán)(Event Loop)和線程池詳解
Node的“事件循環(huán)”(Event Loop)是它能夠處理大并發(fā)、高吞吐量的核心。這是最神奇的地方,據(jù)此Node.js基本上可以理解成“單線程”,同時還允許在后臺處理任意的操作。這篇文章將闡明事件循環(huán)是如何工作的,你也可以感受到它的神奇。
事件驅(qū)動編程
理解事件循環(huán),首先要理解事件驅(qū)動編程(Event Driven Programming)。它出現(xiàn)在1960年。如今,事件驅(qū)動編程在UI編程中大量使用。JavaScript的一個主要用途是與DOM交互,所以使用基于事件的API是很自然的。
簡單地定義:事件驅(qū)動編程通過事件或狀態(tài)的變化來進(jìn)行應(yīng)用程序的流程控制。一般通過事件監(jiān)聽實現(xiàn),一旦事件被檢測到(即狀態(tài)改變)則調(diào)用相應(yīng)的回調(diào)函數(shù)。聽起來很熟悉?其實這就是Node.js事件循環(huán)的基本工作原理。
如果你熟悉客戶端JavaScript的開發(fā),想一想那些.on*()方法,如element.onclick(),他們用來與DOM元素相結(jié)合,傳遞用戶交互。這個工作模式允許在單個實例上觸發(fā)多個事件。Node.js通過EventEmitter(事件發(fā)生器)觸發(fā)這種模式,如在服務(wù)器端的Socket和 “http”模塊中。可以從一個單一實例觸發(fā)一種或一種以上的狀態(tài)改變。
另一種常見的模式是表達(dá)成功succeed和失敗fail。現(xiàn)在一般有兩種常見的實現(xiàn)方式。首先是將“Error異?!眰魅牖卣{(diào),一般作為第一個參數(shù)傳遞給回調(diào)函數(shù)。第二種即使用Promises設(shè)計模式,已經(jīng)加入了ES6。注* Promise模式采用類似jQuery的函數(shù)鏈?zhǔn)綍鴮懛绞?,以避免深層次的回調(diào)函數(shù)嵌套,如:
$.getJSON('/getUser').done(successHandler).fail(failHandler)
“fs”(filesystem)模塊大多采用往回調(diào)中傳入異常的風(fēng)格。在技術(shù)上觸發(fā)某些調(diào)用,例如fs.readFile()附加事件,但該API只是為了提醒用戶,用來表達(dá)操作成功或失敗。選擇這樣的API是出于架構(gòu)的考慮,而非技術(shù)的限制。
一個常見的誤解是,事件發(fā)生器(event emitters)在觸發(fā)事件時也是天生異步的,但這是不正確的。下面是一個簡單的代碼片段,以證明這一點。
function MyEmitter() {
EventEmitter.call(this);
}
util.inherits(MyEmitter, EventEmitter);
MyEmitter.prototype.doStuff = function doStuff() {
console.log('before')
emitter.emit('fire')
console.log('after')}
};
var me = new MyEmitter();
me.on('fire', function() {
console.log('emit fired');
});
me.doStuff();
// 輸出:
// before
// emit fired
// after
注* 如果 emitter.emit 是異步的,則輸出應(yīng)該為
// before
// after
// emit fired
EventEmitter經(jīng)常表現(xiàn)地很異步,因為它經(jīng)常用于通知需要異步完成的操作,但EventEmitter API本身是完全同步的。監(jiān)聽函數(shù)內(nèi)部可以按異步執(zhí)行,但請注意,所有的監(jiān)聽函數(shù)將按被添加的順序同步執(zhí)行。
機(jī)制概述和線程池
Node本身依賴多個庫。其中之一是libuv,神奇的處理異步事件隊列和執(zhí)行的庫。
Node利用盡可能多的利用操作系統(tǒng)內(nèi)核實現(xiàn)現(xiàn)有的功能。像生成響應(yīng)請求(request),轉(zhuǎn)發(fā)連接(connections)并委托給系統(tǒng)處理。例如,傳入的連接通過操作系統(tǒng)進(jìn)行隊列管理,直到它們可以由Node處理。
您可能聽說過,Node有一個線程池,你可能會疑惑:“如果Node會按次序處理任務(wù),為什么還需要一個線程池?”這是因為在內(nèi)核中,不是所有任務(wù)都是按異步執(zhí)行的。在這種情況下,Node.JS必須能在操作時將線程鎖定一段時間,以便它可以繼續(xù)執(zhí)行事件循環(huán)而不會被阻塞。
下面是一個簡單的示例圖,來表示他內(nèi)部的運行機(jī)制:
┌───────────────────────┐
╭──►│ timers │
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ pending callbacks │
│ └───────────┬───────────┘ ┌──────────────┐
│ ┌───────────┴───────────┐ │ incoming: │
│ │ poll │◄──┤ connections, │
│ └───────────┬───────────┘ │ data, etc. │
│ ┌───────────┴───────────┐ └──────────────┘
╰───┤ setImmediate │
└───────────────────────┘
關(guān)于事件循環(huán)的內(nèi)部運行機(jī)制,有一些理解困難的地方:
所有回調(diào)都會經(jīng)由process.nextTick(),在事件循環(huán)(例如,定時器)一個階段的結(jié)束并轉(zhuǎn)換到下一階段之前預(yù)設(shè)定。這就會避免潛在的遞歸調(diào)用process.nextTick(),而造成的無限循環(huán)。
“Pending callbacks(待回調(diào))”,是回調(diào)隊列中不會被任何其他事件循環(huán)周期處理(例如,傳遞給fs.write)的回調(diào)。
Event Emitter 和 Event Loop
通過創(chuàng)建EventEmitter,可簡化與事件循環(huán)的交互。它是一個通用的封裝,可以讓你更容易地創(chuàng)建基于事件的API。關(guān)于這兩者如何互動往往讓開發(fā)者感到混亂。
下面的例子表明,忘記了事件是同步觸發(fā)的,可能導(dǎo)致事件被錯過。
// v0.10以后,不再需要require('events').EventEmitter
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// 抱歉,這個事件永遠(yuǎn)不會發(fā)生
});
上面的'thing1'事件,永遠(yuǎn)不會被MyThing()捕獲,因為MyThing()必須在實例化后才能偵聽事件。下面的是一個簡單的解決方法,不必添加任何額外的閉包:
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);
function emitThing1(self) {
self.emit('thing1');
}
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// 執(zhí)行了
});
下面的方案也可以工作,不過要損失一些性能:
function MyThing() {
EventEmitter.call(this);
doFirstThing();
// 使用 Function#bind() 會損失性能
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);
另一個問題是觸發(fā)Error(異常)。找出您應(yīng)用程序中的問題已經(jīng)很難了,但沒了調(diào)用堆棧(注* e.stack),則幾乎不可能調(diào)試。當(dāng)Error被遠(yuǎn)端的異步請求調(diào)用堆棧將丟失。有兩個可行的解決方案:同步觸發(fā)或確保Error跟其他重要信息一起傳入。下面的例子演示了這兩種解決方案:
MyThing.prototype.foo = function foo() {
// 這個 error 會被異步觸發(fā)
var er = doFirstThing();
if (er) {
// 在觸發(fā)時,需要創(chuàng)建一個新的保留現(xiàn)場調(diào)用堆棧信息的error
setImmediate(emitError, this, new Error('Bad stuff'));
return;
}
// 觸發(fā)error,馬上處理(同步)
var er = doSecondThing();
if (er) {
this.emit('error', 'More bad stuff');
return;
}
}
審時度勢。當(dāng)error被觸發(fā)時,是有可能被立即處理的?;蛘?,它可能是一些瑣碎的,可以很容易處理,或在以后再處理的異常。此外通過一個構(gòu)造函數(shù),傳遞Error也不是一個好主意,因為構(gòu)造出來的對象實例很有可能是不完整的。剛才直接拋出Error的情況是個例外。
結(jié)束語
這篇文章比較淺顯地探討了有關(guān)事件循環(huán)的內(nèi)部運作機(jī)制和技術(shù)細(xì)節(jié)。都是經(jīng)過深思熟慮的。另一篇文章會討論事件循環(huán)與系統(tǒng)內(nèi)核的交互,并展現(xiàn)NodeJS異步運行的魔力。
- nodeJs事件循環(huán)運行代碼解析
- 帶你了解NodeJS事件循環(huán)
- Nodejs監(jiān)控事件循環(huán)異常示例詳解
- 詳解nodejs異步I/O和事件循環(huán)
- 我的Node.js學(xué)習(xí)之路(三)--node.js作用、回調(diào)、同步和異步代碼 以及事件循環(huán)
- 深入理解Node.js 事件循環(huán)和回調(diào)函數(shù)
- 小結(jié)Node.js中非阻塞IO和事件循環(huán)
- 深入淺析Node.js 事件循環(huán)
- 實例分析JS與Node.js中的事件循環(huán)
- nodejs?快速入門之事件循環(huán)
相關(guān)文章
node.js中實現(xiàn)kindEditor圖片上傳功能的方法教程
最近在做一個類似于論壇的系統(tǒng),帖子需要進(jìn)行圖文并茂的顯示,所以用到了富文本編輯器:kindeditor,下面這篇文章主要給大家介紹了在node.js中實現(xiàn)kindEditor圖片上傳功能的方法教程,需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04

