文章目錄
- 1、現實中的發布-訂閱模式
- 2、DOM 事件
- 3、簡單的發布-訂閱模式
- 4、通用的發布-訂閱模式
- 5、先發布再訂閱
- 6、小結
發布—訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都將得到通知。在 JavaScript 開發中,我們一般用事件模型來替代傳統的發布—訂閱模式
1、現實中的發布-訂閱模式
小明最近看上了一套房子,到了售樓處之后才被告知,該樓盤的房子早已售罄。好在售樓
處告訴小明,不久后還有一些尾盤推出。于是小明離開之前,把電話號碼留在了售樓處,相同的還有小紅,小強。于是新樓盤推出的時候,售樓處會翻開花名冊,遍歷上面的電話號碼,依次發送一條短信來通知他們
2、DOM 事件
只要我們曾經在 DOM 節點上面綁定過事件函數,那我們就曾經使用過發布—訂閱模式
document.body.addEventListener( 'click', function(){ alert(2);
}, false ); document.body.click(); // 模擬用戶點擊
3、簡單的發布-訂閱模式
發布-訂閱模式的實現步驟
1、定義發布者
2、給發布者添加一個緩存列表,用于存放回調函數以便通知訂閱者
3、最后發布消息的時候,發布者會遍歷這個緩存列表,依次觸發里面存放的訂閱者回調函數
代碼示例:
var salesOffices = {}; // 定義發布者
salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調函數
salesOffices.listen = function (fn) {// 增加訂閱者this.clientList.push(fn); // 訂閱的消息添加進緩存列表
};
salesOffices.trigger = function () {// 發布消息for (var i = 0; i < this.clientList.length; i++) {var fn = this.clientList.length;fn.apply(this, arguments); // arguments 是發布消息時帶上的參數}
};
測試:
salesOffices.listen(function (price, squareMeter) {// 小明訂閱消息console.log('小明價格= ' + price);console.log('小明squareMeter= ' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) {// 小紅訂閱消息console.log('小紅價格= ' + price);console.log('小紅squareMeter= ' + squareMeter);
});salesOffices.trigger(2000, 300);
salesOffices.trigger(2000, 700);
問題:
訂閱者接收到了發布者發布的每個消息,有些并不是訂閱者需要的
解決:
要增加一個標示 key,讓訂閱者只訂閱自己感興趣的消息:
改寫代碼:
var salesOffices = {}; // 定義發布者
salesOffices.clientList = {}; // 緩存對象,存放訂閱者的回調函數
salesOffices.listen = function (key, fn) {if (!this.clientList[key]) {// 如果還沒有訂閱過此類消息,給該類消息創建一個緩存列表this.clientList[key] = [];}this.clientList[key].push(fn); // 訂閱的消息添加進消息緩存列表
};
salesOffices.trigger = function () {// 發布消息var key = Array.prototype.shift.call(arguments); // 取出消息類型var fns = this.clientList[key]; // 取出該消息對應的回調函數集合if (!fns || fns.length === 0) {// 如果沒有訂閱該消息,則返回return false;}for (var i = 0; i < fns.length; i++) {var fn = fns[i];fn.apply(this, arguments); // (2) // arguments 是發布消息時附送的參數}
};
測試:
salesOffices.listen('squareMeter88', function (price) {// 小明訂閱 88 平方米房子的消息console.log('價格= ' + price); // 輸出: 2000000
});
salesOffices.listen('squareMeter110', function (price) {// 小紅訂閱 110 平方米房子的消息console.log('價格= ' + price); // 輸出: 3000000
});salesOffices.trigger('squareMeter88', 30000);
salesOffices.trigger('squareMeter110', 70000);
4、通用的發布-訂閱模式
包含:發布-訂閱,取消訂閱
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);var fns = this.clientList[key];if (!fns || fns.length === 0) {return false;}for (var i = 0, fn; (fn = fns[i++]); ) {fn.apply(this, arguments);}},// 增加 remove 方法remove(key, fn) {var fns = this.clientList[key];if (!fns) {return false;}if (!fn) {fns && (fns.length = 0);} else {for (var i = fns.length - 1; i >= 0; i--) {var _fn = fns[i];if (fn === _fn) {fns.splice(i, 1);}}}},
};
測試:
var f1 = function (price) {console.log('價格= ' + price);
};
Event.listen('s88', f1);var f2 = function (price) {console.log('價格= ' + price);
};
Event.listen('s110', f2);Event.remove('s110', f2); // 刪除訂閱Event.trigger('s88', 30000);
Event.trigger('s110', 70000);
5、先發布再訂閱
應用場景:發布者發布的內容,不管訂閱者在發布之前訂閱,或者發布之后訂閱,都可觸發訂閱者訂閱的內容
代碼:
var Event = (function () {var clientList = {};var offlineStack = {}; // 離線緩存參數var triggerStack = {}; // 已觸發trigger的參數緩存var listen;var trigger;var remove;listen = function (key, fn) {if (!clientList[key]) {clientList[key] = [];}clientList[key].push(fn);// 如果此時訂閱的事件,已經發布了,則自定觸發一次訂閱內容(fn)if (triggerStack[key]) {fn.apply(this, triggerStack[key]);} else if (offlineStack[key]) {// 如果是離線狀態,則觸發事件fn.apply(this, offlineStack[key]);}};trigger = function () {var key = Array.prototype.shift.call(arguments);var fns = clientList[key];if (fns) {// 已經有人訂閱此事件,將參數緩存//(假如有些訂閱者比較晚訂閱,且發布者已經發布過了,那么這個訂閱者訂閱的時候,自動觸發一次訂閱內容)triggerStack[key] = [...arguments];for (var i = 0; i < fns.length; i++) {fns[i].apply(this, arguments);}} else {// 表示當前還沒有人訂閱此事件,則先將參數緩存起來offlineStack[key] = [...arguments];}};// 取消訂閱remove = function (key, fn) {var fns = this.clientList[key];if (!fns) {return false;}if (!fn) {// 如果沒有傳入具體的回調函數,表示需要取消 key 對應消息的所有訂閱fns && (fns.length = 0);} else {for (var l = fns.length - 1; l >= 0; l--) {var _fn = fns[l];if (_fn === fn) {fns.splice(l, 1);}}}};return {listen: listen,trigger: trigger,remove: remove,};
})();
測試1:先訂閱,再發布
// 先訂閱
Event.listen('test1', function (a) {console.log('我是發布之前的訂閱者1:', a);
});
Event.listen('test1', function (a) {console.log('我是發布之前的訂閱者2:', a);
});
// 再發布
Event.trigger('test1', 12);// 我是發布之前的訂閱者1: 12
// 我是發布之前的訂閱者2: 12
測試2:先發布,再訂閱
// 先發布
Event.trigger('test1', 12);// 再訂閱
Event.listen('test1', function (a) {console.log('我是發布之后的訂閱者1:', a);
});
Event.listen('test1', function (a) {console.log('我是發布之后的訂閱者2:', a);
});// 我是發布之后的訂閱者1: 12
// 我是發布之后的訂閱者2: 12
測試3:先訂閱,再發布,再訂閱
// 先訂閱
Event.listen('lis1', function (a) {console.log('我是發布之前的訂閱者1:', a);
})
Event.listen('lis1', function (a) {console.log('我是發布之前的訂閱者2:', a);
})// 再發布
console.log('---第1次發布');
Event.trigger('lis1', 123);
console.log('---第1次發布完成');// 再訂閱
Event.listen('lis1', function (b) {console.log('我是發布之后的訂閱者~:', b);
})// ---第1次發布
// 我是發布之前的訂閱者1: 123
// 我是發布之前的訂閱者2: 123
// ---第1次發布完成
// 我是發布之后的訂閱者~: 123
測試4:先發布,再訂閱,再發布,再訂閱
// 先發布
console.log('------第1次發布-------');
Event.trigger('lis1', 123);// 再訂閱
Event.listen('lis1', function (a) {console.log('我是發布之后的訂閱者1:', a);
})
Event.listen('lis1', function (a) {console.log('我是發布之后的訂閱者2:', a);
})// 再發布
console.log('------第2次發布-------');
Event.trigger('lis1', 456);// 再訂閱
Event.listen('lis1', function (a) {console.log('我是發布之后的再次訂閱者1:', a);
})
Event.listen('lis1', function (a) {console.log('我是發布之后的再次訂閱者2:', a);
})// ------第1次發布-------
// 我是發布之后的訂閱者1: 123
// 我是發布之后的訂閱者2: 123// ------第2次發布-------
// 我是發布之后的訂閱者1: 456
// 我是發布之后的訂閱者2: 456
// 我是發布z之后的再次訂閱者1: 456
// 我是發布z之后的再次訂閱者2: 456
測試5:先訂閱,再發布,再訂閱,再發布
Event.listen('lis1', function (a) {console.log('我是發布之前的訂閱者1:', a);
})
Event.listen('lis1', function (a) {console.log('我是發布之前的訂閱者2:', a);
})console.log('---第1次發布');
Event.trigger('lis1', 123);
console.log('---第1次發布完成');Event.listen('lis1', function (b) {console.log('我是發布之后的訂閱者~:', b);
})console.log('---第2次發布');
Event.trigger('lis1', 456);
console.log('---第2次發布完成');// ---第1次發布
// 我是發布之前的訂閱者1: 123
// 我是發布之前的訂閱者2: 123
// ---第1次發布完成
// 我是發布之后的訂閱者~: 123// ---第2次發布
// 我是發布之前的訂閱者1: 456
// 我是發布之前的訂閱者2: 456
// 我是發布之后的訂閱者~: 456
// ---第2次發布完成
6、小結
優點:
一為時間上的解耦,二為對象之間的解耦
缺點:
1、創建訂閱者本身要消耗一定的時間和內存,而且當你訂閱一個消息后,也許此消息最后都未發生,但這個訂閱者會始終存在于內存中
2、如果過度使用的話,對象和對象之間的必要聯系也將被深埋在背后,會導致程序難以跟蹤維護和理解
應用:
應用非常廣泛,既可以用在異步編程中,也可以幫助我們完成更松耦合的代碼編寫