第一部分: 發布訂閱模式簡介
發布—訂閱模式又叫觀察者模式,它定義對象間的一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。在javascript開發中,一般用事件模型來替代傳統的發布—訂閱模式。
發布—訂閱模式可以廣泛應用於異步編程中,是一種替代傳遞回調函數的方案。比如,可以訂閱ajax請求的error、success等事件。或者如果想在動畫的每一幀完成之后做一些事情,可以訂閱一個事件,然后在動畫的每一幀完成之后發布這個事件。在異步編程中使用發布—訂閱模式,就無需過多關注對象在異步運行期間的內部狀態,而只需要訂閱感興趣的事件發生點
第二部分:發布訂閱模式在DOM編程操作過程中的使用
發布—訂閱模式可以取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另外一個對象的某個接口。發布—訂閱模式讓兩個對象松耦合地聯系在一起,雖然不太清楚彼此的細節,但這不影響它們之間相互通信。當有新的訂閱者出現時,發布者的代碼不需要任何修改;同樣發布者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們
實際上,前端開發中常用的,在DOM節點上面綁定事件函數,就屬於發布—訂閱模式
document.body.addEventListener('click',function(){ alert(2); },false); document.body.click();
如果需要監控用戶點擊document.body的動作,但是沒辦法預知用戶將在什么時候點擊。所以訂閱document.body上的click事件,當body節點被點擊時,body節點便會向訂閱者發布這個消息。
同理,其實DOM中的很多事件操作都是采用的這個原理
document.body.addEventListener('keyup',function(){ alert(2); },false); document.body.keyup(); document.body.addEventListener('mousedown',function(){ alert(2); },false); document.body.mousedown();
還可以為單個事件添加多個監聽功能,
document.body.addEventListener('click',function(){ console.log(2); },false); document.body.addEventListener('click',function(){ console.log(3); },false); document.body.addEventListener('click',function(){ console.log(4); },false); document.body.click(); //模擬用戶點擊 // 2, 3, 4
第三部分:發布訂閱模式的其他使用場景
假設有一個店鋪,出售Iphone,會有多種型號的Iphone出售,而消費者也會有不同的需求,如果每個消費者都要來向店員詢問自己需要的款型的價格,那么這是一個很低效的行為,因為消費者最關心的就是型號和價格,這樣用發布訂閱模式就最合適不過了
const shop = {}; // 首先定義一個商鋪 shop.list = []; // 定義商鋪里的商品信息列表 shop.listen = function(fn) { // 添加訂閱者 this.list.push(fn); // 將訂閱的商品添加進入商品心里列表 } shop.sell = function(){ for( var i = 0, fn; fn = this.list[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是發布消息參數 } } // 這是來了一個顧客詢問手機的價格,那么 shop.listen(function(iphone, price) { console.log('手機型號' + iphone); console.log('價格' + price) }) // 發布消息,本店賣IphoneX, 價格7000 shop.sell('IphoneX', 7000); shop.sell('Iphone11', 9000); // 輸出 手機型號IphoneX, 價格7000 // 輸出 手機型號Iphone11, 價格9000
現在我們已經實現了一個最簡單的發布訂閱模式了,但這里還存在一些問題。訂閱者接收到了發布者發布的每個消息,如果我只想買Iphone11,我是不關心IphoneX的價格的,但是發布者把IphoneX的信息也推送給了我,這對我來說是不必要的困擾。所以有必要增加一個標示key,讓訂閱者只訂閱自己感興趣的消息。改寫后的代碼如下:
const shop = {}; // 首先定義一個商鋪 shop.list = {}; // 定義商鋪里的商品信息列表 shop.listen = function(key, fn) { // 添加訂閱者 if ( !this.list[key] ){ // 如果沒有訂閱,創建一個緩存列表 this.list[key] = []; } this.list[key].push( fn ); // 訂閱的消息添加進消息緩存列表 } shop.sell = function(){ const key = Array.prototype.shift.call( arguments );// 取出消息 const fns = this.list[ key ]; // 取出該消息對應的回調函數集合 if ( !fns || fns.length === 0 ){ // 如果沒有訂閱該消息,則返回 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是參數 } } // 這是來了一個顧客詢問手機的價格,那么 shop.listen('IphoneX', function(price) { console.log('價格' + price) }) // 這是來了一個顧客詢問手機的價格,那么 shop.listen('Iphone11', function(price) { console.log('價格' + price) }) // 發布消息,本店賣IphoneX, 價格7000 shop.sell('IphoneX', 7000); shop.sell('Iphone11', 9000); // 輸出 價格7000 // 輸出 價格9000
依照上面的例子,我們就可以寫一個基於對象的發布訂閱的模型了
const 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 ), // (1); fns = this.clientList[ key ]; if ( !fns || fns.length === 0 ){ // 如果沒有綁定對應的消息 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments } }, remove: function(key, fn) { var fns = this.clientList[ key ]; if ( !fns ){ // 如果key 被人訂閱,則直接返回 return false; } if ( !fn ){ // 如果沒有傳入具體函數,表示需要取消所有訂閱 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 ); // 刪除訂閱者的回調函數 } } } } };
// 下面是一個基於class的發布訂閱模式的模版,考慮到了邊界條件和匿名函數,屬於一個比較完整的實現
class Pubsub {
constructor () {
}
list = {};
// 添加消息監聽的方法
subscribe (topic, func) {
if (typeof topic !== 'string') {
throw 'topic為字符串類型'
}
if (typeof func !== 'function') {
throw 'func為函數類型'
}
const list = this.list;
if (!list[topic]) {
list[topic] = [];
}
list[topic].push(func);
// 為了防止匿名函數的影響,在添加時將取消監聽的方法返回
return () => this.unsubscribe(topic, func);
}
// 發布消息的方法
publish (topic, data) {
if (typeof topic !== 'string') {
throw 'topic必須是字符串類型'
}
const list = this.list;
if(!list[topic]) {
throw '不存在該事件的監聽'
} else {
list[topic].forEach((func)=>{
func.call(this, data)
})
}
}
// 移除消息監聽的方法
unsubscribe (topic, func){
if(typeof topic !== 'string') {
throw 'topic為字符串類型'
}
if(func && (typeof func !== 'function')) {
throw 'func為函數類型'
}
const list = this.list;
if(!list[topic]) {
throw '不存在該topic監聽'
}
if(!func) { // 如果沒有第二個參數,就移除所有的監聽事件
if(list[topic]) {
delete list[topic]
}
} else {
if(!list[topic].includes(func)) {
throw '要移除的事件不存在'
} else {
const index = list[topic].findIndex(item => item === func);
list[topic].splice(index, 1);
if(list[topic].length === 0) {
delete list[topic]
}
}
}
}
}