從觀察者模式到手寫EventEmitter原始碼
觀察者模式(observer)廣泛的應用於javascript語言中,瀏覽器事件(如滑鼠單擊click,鍵盤事件keyDown)都是該模式的例子。設計這種模式背後的主要原因是促進形成低耦合,在這種模式中不是簡單的物件呼叫物件,而是一個物件“訂閱”另一個物件的某個活動,當物件的活動狀態發生了改變,就去通知訂閱者,而訂閱者也稱為觀察者。
報紙訂閱
生活中就像是去報社訂報紙,你喜歡讀什麼報就去報社去交錢訂閱,當釋出了新報紙的時候,報社會向所有訂閱了報紙的每一個人傳送一份,訂閱者就可以接收到。

我們可以利用這個例子來使用javascript來模擬一下。假設有一個釋出者Jack,它每天出版報紙雜誌,訂閱者Tom將被通知任何時候發生的新聞。
Jack要有一個subscribers屬性,它是一個數組型別,訂閱的行為將會按順序存放在這個陣列中,而通知意味著呼叫訂閱者物件的某個方法。因此,當用戶Tom訂閱資訊的時候,該訂閱者要向Jack的subscribe()提供他的一個方法。當然也可以退訂,我不想再看報紙了,就呼叫unsubscribe()取消訂閱。
一個簡單的觀察者模式應有以下成員:
- subscribes 一個數組
- subscribe() 將訂閱新增到數組裡
- unsubscribe() 把訂閱從陣列中移除
- publish() 迭代陣列,呼叫訂閱時的方法
這個模式中還需要一個type引數,用於區分訂閱的型別,如有的人訂閱的是娛樂新聞,有的人訂閱的是體育雜誌,使用此屬性來標記。
我們使用簡單的程式碼來實現它:
var Jack = { subscribers: { 'any': [] }, //新增訂閱 subscribe: function (type = 'any', fn) { if (!this.subscribers[type]) { this.subscribers[type] = []; } this.subscribers[type].push(fn); //將訂閱方法儲存在數組裡 }, //退訂 unsubscribe: function (type = 'any', fn) { this.subscribers[type] = this.subscribers[type].filter(function (item) { return item !== fn; }); //將退訂的方法從陣列中移除 }, //釋出訂閱 publish: function (type = 'any', ...args) { this.subscribers[type].forEach(function (item) { item(...args);//根據不同的型別呼叫相應的方法 }); } }; 複製程式碼
以上就是一個最簡單的觀察者模式的實現,可以看到程式碼非常的簡單,核心原理就是將訂閱的方法按分類存在一個數組中,當釋出時取出執行即可。
下面使用Tom來訂報:
var Tom = { readNews: function (info) { console.log(info); } }; //Tom訂閱Jack的報紙 Jack.subscribe('娛樂', Tom.readNews); Jack.subscribe('體育', Tom.readNews); //Tom 退訂娛樂新聞: Jack.unsubscribe('娛樂', Tom.readNews); //釋出新報紙: Jack.publish('娛樂', 'S.H.E演唱會驚喜登臺') Jack.publish('體育', '歐國聯-義大利0-1客負葡萄牙'); 複製程式碼
執行結果:
歐國聯-義大利0-1客負葡萄牙 複製程式碼
觀察者模式的實際應用
可以看到觀察者模式將兩個物件的關係變得十分鬆散,當不需要訂閱關係的時候刪掉訂閱的語句即可。那麼在實際應用中有哪些地方使用了這個模式呢?
events模組
node.js的events是一個使用率很高的模組,其它原生node.js模組都是基於它來完成的,比如流、HTTP等,我們可以手寫一版events的核心程式碼,看看觀察者模式的實際應用。
events模組的功能就是一個事件繫結,所有繼承自它的例項都具備事件處理的能力。首先它是一個類,我們寫出它的基本結構:
function EventEmitter() { //私有屬性,儲存訂閱方法 this._events = {}; } //預設最大監聽數 EventEmitter.defaultMaxListeners = 10; module.exports = EventEmitter; 複製程式碼
下面我們一個個將events的核心方法實現。
on方法
首先是on方法,該方法用於訂閱事件,在舊版本的node.js中是addListener方法,它們是同一個函式:
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'就視為插入在陣列的頭部。可以看到,這就是觀察者模式的訂閱方法實現。
emit方法
EventEmitter.prototype.emit = function (type, ...args) { if (this._events[type]) { this._events[type].forEach(fn => fn.call(this, ...args)); } }; 複製程式碼
emit方法就是將訂閱方法取出執行,使用call方法來修正this的指向,使其指向子類的例項。
once方法
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方法非常有趣,它的功能是將事件訂閱“一次”,當這個事件觸發過就不會再次觸發了。其原理是將訂閱的方法再包裹一層函式,在執行後將此函式移除即可。
off方法
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方法即為退訂,原理同觀察者模式一樣,將訂閱方法從陣列中移除即可。
prependListener方法
EventEmitter.prototype.prependListener = function (type, listener) { this.on(type, listener, true); }; 複製程式碼
此方法不必多說了,呼叫on方法將標記傳為true(插入訂閱方法在頭部)即可。
以上,就將EventEmitter類的核心方法實現了。