一、定義
又叫觀察者模式,他定義物件間的依照那個一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將的到通知。在javascript中,我們一般用時間模型來替代傳統的釋出-訂閱模式
二、舉例
js中對dom元素繫結事件監聽,就是簡單的釋出-訂閱。另外在很多的框架和外掛中都會存在使用這種方式來,比如vue的watch
三、結構
釋出-訂閱模式可以分為兩種形式,一種是釋出者和訂閱者直接進行通訊,其結構如下:
另一種是通過中介進行通訊,釋出者和訂閱者互不相知,其結構如下:
四、實現
1.釋出者和訂閱者直接進行通訊
這種模式的核心在於,要在釋出者中維護儲存一個訂閱者的回撥函式的陣列。
典型的例子就是繫結dom元素,但是js未將對應的釋出者暴露,這是瀏覽器實現的,因此我們使用的都是直接進行訂閱。我們可以自定義一個事件,程式碼如下:
// 釋出者 var publisher = { clientList: {}, listen: function(key, fn){ if(!this.clientList[key]) { this.clientList[key] = []; } this.clientList[key].push(fn); }, trigger: function(){ var key = Array.prototype.shift.call(arguments), // 獲取釋出的事件名稱 fns = this.clientList[key]; // 獲取該事件下所有的回撥函式列表 if(!fns || fns.length === 0){ return false; } for(var i = 0, l = fns.length; i < l; i++){ fns[i].apply(this, arguments); } }, run: function(){ // 釋出者根據實際情況在合適時機發布事件 this.trigger("start_load", "開始載入"); console.log("start load"); this.trigger("loading", "正在載入"); console.log("loading..."); this.trigger("finish_load", "載入完成"); console.log("finish load"); } }; // 訂閱者 var subscriber = { init: function(){ // 訂閱 publisher.listen("finish_load", function(rst){ console.log("我是訂閱者,我訂閱了釋出者的載入完成事件,現在我收到了釋出者的資訊:" + rst); }); } }; subscriber.init(); publisher.run();
這種的模式很簡單,但是他的缺點在於如果有多個釋出者,那麼就需要讓每個釋出者維護listen、trigger函式和一個事件回撥函式快取列表,比如我們可以會對js檔案的載入過程進行訂閱,也可能會對dom的構建過程進行訂閱等等,顯然每個釋出者分別建立一個物件是耗費記憶體也是不優雅的。另外這種模式存在著釋出者和訂閱這的耦合性,往往在開發過程中,我們可能根本沒有必要讓釋出者和訂閱者進行通訊,各自做好自己的事情就好了。因此這種方式很少會用到。
2.通過中介進行通訊
針對上面的方式的缺點,就有了這種方式,這樣的模式是一種全域性的釋出-訂閱模式。其核心是建立一箇中介,也就是一個全域性的Event物件,讓他來幫助釋出者和訂閱者溝通。程式碼如下:
// 事件物件,作為中介 var Event = { clientList: {}, listen: function(key, fn){ if(!this.clientList[key]) { this.clientList[key] = []; } this.clientList[key].push(fn); }, trigger: function(){ var key = Array.prototype.shift.call(arguments), // 獲取釋出的事件名稱 fns = this.clientList[key]; // 獲取該事件下所有的回撥函式列表 if(!fns || fns.length === 0){ return false; } for(var i = 0, l = fns.length; i < l; i++){ fns[i].apply(this, arguments); } } }; // 釋出者 var publisher = { run: function(){ Event.trigger("start_load", "開始載入"); console.log("start load"); Event.trigger("loading", "正在載入"); console.log("loading..."); Event.trigger("finish_load", "載入完成"); console.log("finish load"); } }; // 訂閱者 var subscriber = { init: function(){ // 訂閱 Event.listen("finish_load", function(rst){ console.log("我是訂閱者,我訂閱了釋出者的載入完成事件,現在我收到了釋出者的資訊:" + rst); }); } }; subscriber.init(); publisher.run();
我們看繪製二維地圖的leaflet框架中,對於釋出-訂閱模式的實現:
export var Events = { // 新增監聽事件,types:{mouseclick: fn, dbclick: fn} 或者:"mouseclick dbclick" on: function (types, fn, context) {...}, // 移除事件,若未設定任何引數,則刪除該物件所有的事件。若fn未設定,則刪除物件中所有的type事件 off: function (types, fn, context) {...}, // 內部註冊監聽事件 _on: function (type, fn, context) { this._events = this._events || {}; var typeListeners = this._events[type]; // 獲取物件中其他註冊的該事件的回撥函式 // 若物件中未曾設定過相同事件名稱,則儲存其回撥函式 if (!typeListeners) { typeListeners = []; this._events[type] = typeListeners; } var newListener = {fn: fn, ctx: context}; ... typeListeners.push(newListener); }, // 移除事件, 若未設定fn則刪除所有的type事件。,否則刪除對應的事件 _off: function (type, fn, context) {...}, // 觸發物件中的所有type事件,若設定了propagate則觸發父物件的type事件 fire: function (type, data, propagate) { if (!this.listens(type, propagate)) { return this; } // 檢查是否註冊了type事件 // 構建回撥函式中引數事件物件 var event = Util.extend({}, data, { type: type, target: this, sourceTarget: data && data.sourceTarget || this }); if (this._events) { var listeners = this._events[type]; // _firingCount用於防止觸發事件未執行完成同時刪除該事件。 // _firingCound表示正在執行的回撥函式的個數,當為0時表示沒有正在執行的事件。可以直接刪除,否則需要將_events進行復制,防止刪除掉需要回撥的物件 if (listeners) { this._firingCount = (this._firingCount + 1) || 1; // 執行物件註冊的所有該事件的回撥函式 for (var i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; l.fn.call(l.ctx || this, event); } this._firingCount--; } } return this; }, listens: function (type) { var listeners = this._events && this._events[type]; return !!(listeners && listeners.length); }, }; export var Evented = Class.extend(Events);
使用Event基礎類用來實現釋出-訂閱,而這個基礎類類似於一個介面,他需要依附於一個實際的物件來構造該物件的事件系統,比如框架中的圖層需要有各種滑鼠、鍵盤觸控事件,因此為了模仿類似dom一樣的觸發方式,圖層類就要繼承這個Event類:
因此在訂閱的時候,就可以直接:
layer.on("click",function(){});
五、總結
釋出-訂閱模式的關鍵在於用一個事件物件對其進行實現,其中需要有以下幾點:
1.快取物件:用於儲存訂閱者監聽的回撥函式,鍵為事件名稱,值為該事件名下所有的回撥函式,其結構形如:
var cache = { "click": [fn1,fn2,fn3...], "dbclick": [fn4,fn5,fn6...] ... }
2.listen/on函式:監聽函式,為訂閱者使用,通常包含兩個引數:事件名稱和回撥函式,內部會將這兩個引數儲存到快取物件中
3.trigger/fire函式:釋出函式,為釋出者使用,通常包含一個引數:事件名稱。內部通過事件名稱到快取物件中查詢對應的回撥函式陣列,並依次執行
4.remoe/off函式:接觸監聽函式,為訂閱者使用,通常包含一個或兩個引數:事件名稱或註冊監聽時的回撥函式。若為一個引數事件名稱,則會到快取函式中查詢到對應的註冊的回撥函式的陣列,並將其清空。若有第二個引數回撥函式,會在快取物件中找到事件名稱對應的回撥函式陣列,查詢是否存在引數中的回撥函式,有的話,則只刪除這一個回撥函式