觀察者模式(observer)普遍的應用於javascript語言中,瀏覽器事件(如鼠標單擊click,鍵盤事件keyDown)都是該模式的例子。設計這種模式背後的主要緣由是促進造成低耦合,在這種模式中不是簡單的對象調用對象,而是一個對象「訂閱」另外一個對象的某個活動,當對象的活動狀態發生了改變,就去通知訂閱者,而訂閱者也稱爲觀察者。javascript
生活中就像是去報社訂報紙,你喜歡讀什麼報就去報社去交錢訂閱,當發佈了新報紙的時候,報社會向全部訂閱了報紙的每個人發送一份,訂閱者就能夠接收到。java
咱們能夠利用這個例子來使用javascript來模擬一下。假設有一個發佈者Jack,它天天出版報紙雜誌,訂閱者Tom將被通知任什麼時候候發生的新聞。node
Jack要有一個subscribers屬性,它是一個數組類型,訂閱的行爲將會按順序存放在這個數組中,而通知意味着調用訂閱者對象的某個方法。所以,當用戶Tom訂閱信息的時候,該訂閱者要向Jack的subscribe()提供他的一個方法。固然也能夠退訂,我不想再看報紙了,就調用unsubscribe()取消訂閱。數組
一個簡單的觀察者模式應有如下成員:瀏覽器
這個模式中還須要一個type參數,用於區分訂閱的類型,若有的人訂閱的是娛樂新聞,有的人訂閱的是體育雜誌,使用此屬性來標記。函數
咱們使用簡單的代碼來實現它:this
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); //根據不一樣的類型調用相應的方法 }); } };
以上就是一個最簡單的觀察者模式的實現,能夠看到代碼很是的簡單,核心原理就是將訂閱的方法按分類存在一個數組中,當發佈時取出執行便可。spa
下面使用Tom來訂報:prototype
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客負葡萄牙
能夠看到觀察者模式將兩個對象的關係變得十分鬆散,當不須要訂閱關係的時候刪掉訂閱的語句便可。那麼在實際應用中有哪些地方使用了這個模式呢?
node.js的events是一個使用率很高的模塊,其它原生node.js模塊都是基於它來完成的,好比流、HTTP等,咱們能夠手寫一版events的核心代碼,看看觀察者模式的實際應用。
events模塊的功能就是一個事件綁定,全部繼承自它的實例都具有事件處理的能力。首先它是一個類,咱們寫出它的基本結構:
function EventEmitter() { //私有屬性,保存訂閱方法 this._events = {}; } //默認最大監聽數 EventEmitter.defaultMaxListeners = 10; module.exports = EventEmitter;
下面咱們一個個將events的核心方法實現。
首先是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'就視爲插入在數組的頭部。能夠看到,這就是觀察者模式的訂閱方法實現。
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類的核心方法實現了。
經過建立「可觀察的」對象,當發生一個感興趣的事件時可將該事件通告給全部觀察者,從而造成鬆散的耦合。
部分實例參考《JavaScript模式》做者:Stoyan Stefanov