發布/訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都將得到通知。在 JavaScript 開發中,我們一般用事件模型來替代傳統的發布/訂閱模式。
遍地的發布訂閱現象
如今的信息化時代,發布/訂閱模式的應用可以說非常廣泛,比如微信公眾號就是典型的發布/訂閱模式,公眾號發布一條信息,所有的訂閱者都會收到。
有人可能也會想到經常收到的各種廣告短信信息(有的可能是被動訂閱),其實發送短信通知或廣告也是一個典型的發布/訂閱模式。
發布/訂閱模式可以廣泛用于異步編程中,代替傳遞回掉函數的方案,比如,我們可以訂閱 ajax 請求的 error、succ 等事件。
另外發表訂閱讓兩個對象松耦合在一起,不必了解彼此細節,當有新的訂閱者出現時,發布者的代碼不需要任何修改。同樣發布者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。
定義
發布訂閱模式,它定義了一種一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。
使用發布訂閱模式的好處:
- 支持簡單的廣播通信,自動通知所有已經訂閱過的對象。
- 頁面載入后目標對象很容易與觀察者存在一種動態關聯,增加了靈活性。
- 目標對象與觀察者之間的抽象耦合關系能夠單獨擴展以及重用。
使用實例
自定義發表訂閱
我們來嘗試一個自定義的發布訂閱模式,那么如何實現發布訂閱呢
- 指定一個發布者
- 給發布者添加一個緩沖列表,用于存放回調函數以用于通知訂閱者
- 發布消息時,發布者遍歷緩存列表,依次觸發每個訂閱者的回調函數
一個簡單的天氣狀態訂閱
var Weather = {
list: [], // 緩存列表
listen: function(fn) { // 增加訂閱者
this.list.push(fn)
},
publish: function() { // 發布消息
for(var i=0,fn; fn=this.list[i++];) {
fn.apply(this,arguments);
}
}
};
// 訂閱消息
Weather.listen(function(weather, wind){
console.log('天氣:' + weather, '風力:'+ wind);
})
// 發布消息
Weather.publish("晴天","微風"); // 天氣:晴天 風力:微風
Weather.publish("雷陣雨","5級風"); // 天氣:雷陣雨 風力:5級風
以上,已經實現了一個最簡單的發布—訂閱模式,還可以為訂閱者增加自選功能,訂閱自己想要的消息,也可以增加取消訂閱的事件。
var PubSub = {
list: [],
listen: function(key, fn){
if(!this.list[key]) {
this.list[key]=[];
}
this.list[key].push(fn);
},
publish: function(){
var key = Array.prototype.shift.call(arguments),
fns = this.list[key];
if(!fns || fns.length === 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];){
fn.apply(this, arguments);
}
}
}
//
var installEvent = function(obj) {
for (var i in PubSub) {
obj[i] = PubSub[i];
}
};
var day = {}
installEvent(day);
day.listen('天氣', function(wind) {
console.log('風力:'+ wind);
});
day.publish('天氣', "8級風");
實戰之網站登錄
網站登錄是最常見的形式,通常在登錄以后我們會ajax異步請求獲取用戶信息,比如顯示用戶名字、頭像等信息在header模塊,而這兩個字段都是來自用戶登錄后返回的信息。至于 ajax 請求什么時候能成功返回用戶信息,這點我們沒有辦法確定,雖然現在看起來和發布訂閱模式沒關系,因為異步的問題通常也可以回調函數來解決。
我們不知道除了 header 頭部、nav 導航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息。如果它們和用戶信息模塊產生了強耦合,比如下面這樣的形式:
login.succ(function(data){
header.setAvatar( data.avatar); // 設置 header 模塊的頭像
nav.setAvatar( data.avatar ); // 設置導航模塊的頭像
message.refresh(); // 刷新消息列表
cart.refresh(); // 刷新購物車列表
});
現在登錄模塊是我們負責編寫的,但我們還必須了解 header 模塊里設置頭像的方法叫 setAvatar、購物車模塊里刷新的方法叫 refresh,這種耦合性會使程序變得僵硬,header 模塊不能隨意再改變 setAvatar 的方法名,它自身的名字也不能被改為 header1、header2。 這是針對具 體實現編程的典型例子,針對具體實現編程是不被贊同的。
某一個,項目新增加收獲地址管理模塊:
login.succ(function(data){
header.setAvatar( data.avatar);
nav.setAvatar( data.avatar );
message.refresh();
address.refresh(); // 新增加收獲地址
});
現在我們用發布訂閱重寫,對用戶信息感興趣的業務模塊將自行訂閱登錄成功的消息事件。 當登錄成功時,登錄模塊只需要發布登錄成功的消息,而業務方接受到消息之后,就會開始進行各自的業務處理,登錄模塊并不關心業務方究竟要做什么,也不想去了解它們的內部細節。改善后的代碼如下:
$.ajax( 'http://xxx.com?login', function(data){ // 登錄成功
login.trigger( 'loginSucc', data); // 發布登錄成功的消息
});
各模塊監聽登錄成功的消息:
var header = (function() { // header 模塊
login.listen( 'loginSucc', function(data) {
header.setAvatar( data.avatar );
});
return {
setAvatar: function(data) {
console.log( '設置 header 模塊的頭像');
}
}
})();
var nav = (function() { // nav 模塊
login.listen('loginSucc', function(data) {
nav.setAvatar( data.avatar );
});
return {
setAvatar: function(avatar) {
console.log( '設置 nav 模塊的頭像');
}
}
})();
如果有一天在登錄完成之 后,又增加一個刷新收貨地址列表的行為,那么只要在收貨地址模塊里加上監聽消息的方法即可,而這可以讓開發該模塊的同事自己完成,你作為登錄模塊的開發者,永遠不用再關心這些行為了
var address = (function(){ // 收獲地址模塊
login.listen('loginSucc', function(obj){
address.refresh(obj);
});
return {
refresh: function( avatar ){
console.log( '刷新收貨地址列表' );
}
}
})();
總結
發布訂閱的使用場合就是:當一個對象的改變需要同時改變其它對象,并且它不知道具體有多少對象需要改變的時候,就應該考慮使用觀察者模式。
總的來說,發布訂閱模式所做的工作就是在解耦,讓耦合的雙方都依賴于抽象,而不是依賴于具體。從而使得各自的變化都不會影響到另一邊的變化。
另外, 發布—訂閱模式雖然可以弱化對象之間的聯系,但如果過度使用的話,對象和對象之間的必要聯系也將被深埋在背后,會導致程序難以跟蹤維護和理解。特別是有多個發布者和訂閱者嵌套到一起的時候,要跟蹤一個 bug 不是件輕松的事情。
參考引用資料
《JavaScript設計模式與開發實踐》