爲何寫這篇文章?javascript
做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…前端
說一下 Node.js 哪裏應用到了發佈/訂閱模式java
Events 模塊在實際項目開發中有使用過嗎?具體應用場景是?node
Events 監聽函數的執行順序是異步仍是同步的?git
說幾個 Events 模塊的經常使用函數吧?程序員
模擬實現 Node.js 的核心模塊 Eventsgithub
文章首發Github 博客開源項目 github.com/koala-codin…面試
發佈/訂閱者模式
應該是我在開發過程當中遇到的最多的設計模式。發佈/訂閱者模式
,也能夠稱之爲消息機制,定義了一種依賴關係,這種依賴關係能夠理解爲 1對N
(注意:不必定是1對多,有時候也會1對1哦),觀察者們同時監聽某一個對象相應的狀態變換,一旦變化則通知到全部觀察者,從而觸發觀察者相應的事件,該設計模式解決了主體對象與觀察者之間功能的耦合
。數據庫
在現實生活中,警察抓小偷是一個典型的觀察者模式「這以一個慣犯在街道逛街而後被抓爲例子」,這裏小偷就是被觀察者,各個幹警就是觀察者,幹警時時觀察着小偷,當小偷正在偷東西「就給幹警發送出一條信號,實際上小偷不可能告訴幹警我有偷東西」,幹警收到信號,出擊抓小偷。這就是一個觀察者模式編程
生活中就像是去報社訂報紙,你喜歡讀什麼報就去報社去交錢訂閱,當發佈了新報紙的時候,報社會向全部訂閱了報紙的每個人發送一份,訂閱者就能夠接收到。
我這個微信公號做者是發佈者,您這些微信用戶是訂閱者「我發送一篇文章的時候,關注了【程序員成長指北】的訂閱者們均可以收到文章。
以你們訂閱公衆號
爲例子,看看發佈/訂閱模式
如何實現的。(以訂閱報紙做爲例子的緣由,能夠增長一個type
參數,用於區分訂閱不一樣類型的公衆號,若有的人訂閱的是前端公衆號,有的人訂閱的是 Node.js 公衆號,使用此屬性來標記。這樣和接下來要講的 EventEmitter 源碼更相符,另外一個緣由是這樣你只要打開一個訂閱號文章是否是就想到了發佈-訂閱者模式呢。)
代碼以下:
let officeAccounts ={
// 初始化定義一個存儲類型對象
subscribes:{
'any':[]
},
// 添加訂閱號
subscribe:function(type='any',fn){
if(!this.subscribes[type]){
this.subscribes[type] = [];
}
this.subscribes[type].push(fn);//將訂閱方法存在數組中
},
// 退訂
unSubscribe:function(type='any',fn){
this.subscribes[type] =
this.subscribes[type].filter((item)=>{
return item!=fn;// 將退訂的方法從數組中移除
});
},
// 發佈訂閱
publish:function(type='any',...args){
this.subscribes[type].forEach(item => {
item(...args);// 根據不一樣的類型調用相應的方法
});
}
}
複製代碼
以上就是一個最簡單的觀察者模式的實現,能夠看到代碼很是的簡單,核心原理就是將訂閱的方法按分類存在一個數組中,當發佈時取出執行便可
接下里看小明訂閱【程序員成長指北】文章的代碼:
let xiaoming = {
readArticle:function (info) {
console.log('小明收到的',info);
}
};
let xiaogang = {
readArticle:function (info) {
console.log('小剛收到的',info);
}
};
officeAccounts.subscribe('程序員成長指北',xiaoming.readArticle);
officeAccounts.subscribe('程序員成長指北',xiaogang.readArticle);
officeAccounts.subscribe('某公衆號',xiaoming.readArticle);
officeAccounts.unSubscribe('某公衆號',xiaoming.readArticle);
officeAccounts.publish('程序員成長指北','程序員成長指北的Node文章');
officeAccounts.publish('某公衆號','某公衆號的文章');
複製代碼
運行結果:
小明收到的 程序員成長指北的Node文章
小剛收到的 程序員成長指北的Node文章
複製代碼
經過觀察現實生活中的三個例子以及代碼實例發現發佈/訂閱模式的確是1對N的關係。當發佈者的狀態發生改變時,全部訂閱者都會獲得通知。
主體和觀察者之間徹底透明,全部的消息傳遞過程都經過消息調度中心完成,也就是說具體的業務邏輯代碼將會是在消息調度中心內,而主體和觀察者之間實現了徹底的鬆耦合。對象直接的解耦,異步編程中,能夠更鬆耦合的代碼編寫。
程序易讀性顯著下降;多個發佈者和訂閱者嵌套在一塊兒的時候,程序難以跟蹤,其實仍是代碼不易讀,嘿嘿。
Node.js 中的 EventEmitter 模塊就是用了發佈/訂閱這種設計模式,發佈/訂閱 模式在主體與觀察者之間引入消息調度中心,主體和觀察者之間徹底透明,所 有的消息傳遞過程都經過消息調度中心完成,也就是說具體的業務邏輯代碼將會是在消息調度中心內完成。
Events是 Node.js 中一個使用率很高的模塊,其它原生node.js模塊都是基於它來完成的,好比流、HTTP等。它的核心思想就是 Events 模塊的功能就是一個事件綁定與觸發
,全部繼承自它的實例都具有事件處理的能力。
本模塊的官方 Api 講解不是直接帶你們學習文檔,而是 經過對比
發佈/訂閱設計模式本身手寫一個版本 Events 的核心代碼來學習並記住Api
Events 模塊只有一個 EventEmitter 類,首先定義類的基本結構
function EventEmitter() {
//私有屬性,保存訂閱方法
this._events = {};
}
//默認設置最大監聽數
EventEmitter.defaultMaxListeners = 10;
module.exports = EventEmitter;
複製代碼
on 方法,該方法用於訂閱事件(這裏 on 和 addListener 說明下),Node.js 源碼中這樣把它們倆賦值了下,我也不太懂爲何?知道的小夥伴能夠告訴我爲何要這樣作哦。
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
複製代碼
接下來是咱們對on方法的具體實踐:
EventEmitter.prototype.on =
EventEmitter.prototype.addListener = function (type, listener, flag) {
//保證存在實例屬性
if (!this._events) this._events = Object.create(null);
if (this._events[type]) {
if (flag) {//從頭部插入
this._events[type].unshift(listener);
} else {
this._events[type].push(listener);
}
} else {
this._events[type] = [listener];
}
//綁定事件,觸發newListener
if (type !== 'newListener') {
this.emit('newListener', type);
}
};
複製代碼
由於有其它子類須要繼承自EventEmitter,所以要判斷子類是否存在_event屬性,這樣作是爲了保證子類必須存在此實例屬性。而flag標記是一個訂閱方法的插入標識,若是爲'true'就視爲插入在數組的頭部。能夠看到,這就是觀察者模式的訂閱方法實現。
EventEmitter.prototype.emit = function (type, ...args) {
if (this._events[type]) {
this._events[type].forEach(fn => fn.call(this, ...args));
}
};
複製代碼
emit方法就是將訂閱方法取出執行,使用call方法來修正this的指向,使其指向子類的實例。
EventEmitter.prototype.once = function (type, listener) {
let _this = this;
//中間函數,在調用完以後當即刪除訂閱
function only() {
listener();
_this.removeListener(type, only);
}
//origin保存原回調的引用,用於remove時的判斷
only.origin = listener;
this.on(type, only);
};
複製代碼
once方法很是有趣,它的功能是將事件訂閱「一次」,當這個事件觸發過就不會再次觸發了。其原理是將訂閱的方法再包裹一層函數,在執行後將此函數移除便可。
EventEmitter.prototype.off =
EventEmitter.prototype.removeListener = function (type, listener) {
if (this._events[type]) {
//過濾掉退訂的方法,從數組中移除
this._events[type] =
this._events[type].filter(fn => {
return fn !== listener && fn.origin !== listener
});
}
};
複製代碼
off方法即爲退訂,原理同觀察者模式同樣,將訂閱方法從數組中移除便可。
EventEmitter.prototype.prependListener = function (type, listener) {
this.on(type, listener, true);
};
複製代碼
碼此方法沒必要多說了,調用on方法將標記傳爲true(插入訂閱方法在頭部)便可。 以上,就將EventEmitter類的核心方法實現了。
emitter.listenerCount(eventName)
能夠獲取事件註冊的listener
個數emitter.listeners(eventName)
能夠獲取事件註冊的listener
數組副本。//event.js 文件
var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener1', arg1, arg2);
});
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener2', arg1, arg2);
});
emitter.emit('someEvent', 'arg1 參數', 'arg2 參數');
複製代碼
執行以上代碼,運行的結果以下:
$ node event.js
listener1 arg1 參數 arg2 參數
listener2 arg1 參數 arg2 參數
複製代碼
手寫Events模塊代碼的時候注意如下幾點:
注意:我上面的手寫代碼並非性能最好和最完善的,目的只是帶你們先弄懂記住他。舉個例子: 最初的定義EventEmitter類,源碼中並非直接定義 this._events = {}
,請看:
function EventEmitter() {
EventEmitter.init.call(this);
}
EventEmitter.init = function() {
if (this._events === undefined ||
this._events === Object.getPrototypeOf(this)._events) {
this._events = Object.create(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
};
複製代碼
一樣是實現一個類,可是源碼中更注意性能,咱們可能認爲簡單的一個 this._events = {}
;就能夠了,可是經過jsperf
(一個小彩蛋,有須要的搜如下,查看性能工具) 比較二者的性能,源碼中高了不少,我就不具體一一講解了,附上源碼地址,有興趣的能夠去學習
lib/events源碼地址 github.com/nodejs/node…
源碼篇幅過長,給了地址能夠對比繼續研究,畢竟是公衆號文章,不想被說。可是一些疑問仍是要講的,嘿嘿。
看一段代碼:
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
console.log('listener1');
});
myEmitter.on('event', async function() {
console.log('listener2');
setTimeout(() => {
console.log('我是異步中的輸出');
resolve(1);
}, 1000);
});
myEmitter.on('event', function() {
console.log('listener3');
});
myEmitter.emit('event');
console.log('end');
複製代碼
輸出結果以下:
// 輸出結果
listener1
listener2
listener3
end
我是異步中的輸出
複製代碼
EventEmitter觸發事件的時候,各監聽函數的調用
是同步的(注意:監聽函數的調用是同步的,'end'的輸出在最後),可是並非說監聽函數裏不能包含異步的代碼,代碼中listener2那個事件就加了一個異步的函數,它是最後輸出的。
我爲何要把這個單獨寫成一個小標題來說,由於發現網上好多文章都是錯的,或者不明確,給你們形成了誤導。
看這裏,某API網站的一段話,具體網站名稱在這裏就不說了,不想招黑,這段內容沒問題,可是對於剛接觸事件機制的小夥伴容易混淆
fs.open
爲例子,看一下到底何時產生了事件,何時觸發,和EventEmitter有什麼關係呢?
流程的一個說明:本圖中詳細繪製了從 異步調用開始--->異步調用請求封裝--->請求對象傳入I/O線程池完成I/O操做--->將完成的I/O結果交給I/O觀察者--->從I/O觀察者中取出回調函數和結果調用執行。
關於事件你看圖中第三部分,事件循環那裏。Node.js 全部的異步 I/O 操做(net.Server, fs.readStream 等)在完成後
都會添加一個事件到事件循環的事件隊列中。
事件的觸發,咱們只須要關注圖中第三部分,事件循環會在事件隊列中取出事件處理。fs.open
產生事件的對象都是 events.EventEmitter 的實例,繼承了EventEmitter,從事件循環取出事件的時候,觸發這個事件和回調函數。
越寫越多,越寫越想,老是這樣,須要控制一下。
當咱們直接爲EventEmitter定義一個error事件,它包含了錯誤的語義,咱們在遇到 異常的時候一般會觸發 error 事件。
當 error 被觸發時,EventEmitter 規定若是沒有響 應的監聽器,Node.js 會把它看成異常,退出程序並輸出錯誤信息。
var events = require('events');
var emitter = new events.EventEmitter();
emitter.emit('error');
複製代碼
運行時會報錯
node.js:201
throw e; // process.nextTick error, or 'error' event on first tick
^
Error: Uncaught, unspecified 'error' event.
at EventEmitter.emit (events.js:50:15)
at Object.<anonymous> (/home/byvoid/error.js:5:9)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)
複製代碼
咱們通常要爲會觸發 error 事件的對象設置監聽器,避免遇到錯誤後整個程序崩潰。
默認狀況下針對單一事件的最大listener數量是10,若是超過10個的話listener仍是會執行,只是控制檯會有警告信息,告警信息裏面已經提示了操做建議,能夠經過調用emitter.setMaxListeners()來調整最大listener的限制
(node:9379) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
複製代碼
上面的警告信息的粒度不夠,並不能告訴咱們是哪裏的代碼出了問題,能夠經過process.on('warning')來得到更具體的信息(emitter、event、eventCount)
process.on('warning', (e) => {
console.log(e);
})
{ MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
at _addListener (events.js:289:19)
at MyEmitter.prependListener (events.js:313:14)
at Object.<anonymous> (/Users/xiji/workspace/learn/event-emitter/b.js:34:11)
at Module._compile (module.js:641:30)
at Object.Module._extensions..js (module.js:652:10)
at Module.load (module.js:560:32)
at tryModuleLoad (module.js:503:12)
at Function.Module._load (module.js:495:3)
at Function.Module.runMain (module.js:682:10)
at startup (bootstrap_node.js:191:16)
name: 'MaxListenersExceededWarning',
emitter:
MyEmitter {
domain: null,
_events: { event: [Array] },
_eventsCount: 1,
_maxListeners: undefined },
type: 'event',
count: 11 }
複製代碼
fs
模塊 net
模塊觀察者模式與發佈-訂閱者模式,在平時你能夠認爲他們是一個東西,可是在某些場合(好比面試)可能須要稍加註意,看一下兩者的區別對比
借用網上的一張圖
參考文章:
加入咱們一塊兒學習吧!