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