觀察者模式又叫做發(fā)布-訂閱模式。這是一種一對多的對象依賴關(guān)系,當(dāng)被依賴的對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知。
生活中的觀察者模式
就如我們在專賣店預(yù)定商品(如:蘋果手機),我們會向?qū)Yu店提交預(yù)定申請,然后店家受申請,正常這樣就完事了。假如,近段時間蘋果手機的需求很大,而商品有限,那么商家就會要這些果粉預(yù)留電話等待通知,等到手機一到,商家就會遍歷果粉預(yù)留信息,然后發(fā)通知給這些果粉。生活中商家強調(diào)客戶在家等通知即可,并且說一有消息就會通知客戶,而不會傻到要客戶主動打電話詢問,這樣不僅客戶的代價比較大,商家的負(fù)荷更大,用戶的輪詢方式也從打電話變成了查看短信息。
觀察者模式的優(yōu)勢
發(fā)布和訂閱這兩個對象是松耦合地聯(lián)系在一起的,它們不用彼此熟悉內(nèi)部的實現(xiàn)細(xì)節(jié),但這不影響它們之間的通信,它們只要知道彼此需要做什么就行。當(dāng)有新訂閱者增加時,發(fā)布者不需要任何更改,同樣的當(dāng)發(fā)布者改變時,訂閱者也不會受到影響。
就像新聞聯(lián)播一樣里面的央視主持人換了,也不影響我們看央視的新聞聯(lián)播,同樣你看或不看新聞聯(lián)播,對央視來說也無影響。
在異步通信中觀察者模式也是大有好處,發(fā)布者只需按順序的發(fā)布事件即可,而訂閱者只需在異步運行期間訂閱相關(guān)事件即可。
JavaScript中的觀察者模式
在JavaScript中觀察者模式的實現(xiàn)主要用事件模型。
DOM事件
document.body.addEventListener('click', function() {
console.log('hello world!');
});
相信這樣的代碼不少的同學(xué)都寫過,但我要說這其實就是一種觀察者模式的實現(xiàn),可能一些童鞋還不信,那么看一看修改后的代碼。
// 發(fā)布者
var pub = function() {
console.log('歡迎訂閱!')
}
// 訂閱者
var sub = document.body;
// 訂閱者實現(xiàn)訂閱
sub.addEventListener('click', pub, false);
訂閱者可以任意的添加,發(fā)布者也可以隨意的修改。
自定義事件
雖然,使用dom事件可以輕松解決我們開發(fā)中的一部分問題;但是還有一些問題需要我們使用自定義事件來完成。
那面就說一說如何用自定義事件實現(xiàn)代理。
我們還以預(yù)定手機為例,參考dom事件的原理來實現(xiàn)觀察者模式,用用戶的電話號碼作為類型,用戶的定購信息用一個回調(diào)函數(shù)來表示。
基本概念定義如下:
- 商家: 發(fā)布者
- 客戶: 訂閱者
- 緩存列表:記錄客戶的電話,方便商家遍歷發(fā)通知消息給客戶
注:緩存列表,我將它定義為一個對象,用戶的電話號碼作為key,用戶的預(yù)定信息是個數(shù)組作為value。
代碼實現(xiàn)如下:
// 定義商家
var merchants = {};
// 定義預(yù)定列表
merchants.orderList = {};
// 將增加的預(yù)訂者添加到預(yù)定客戶列表中
merchants.listen = function(id, info) {
if(!this.orderList[id]) {
this.orderList[id] = [];
}
this.orderList[id].push(info);
console.log('預(yù)定成功')
};
//發(fā)布消息
merchants.publish = function() {
var id = Array.prototype.shift.call(arguments);
var infos = this.orderList[id];
// 判斷是否有預(yù)訂信息
if(!infos || infos.length === 0) {
console.log('您還沒有預(yù)訂信息!');
return false;
}
// 如果有預(yù)訂信息,則循環(huán)打印
for (var i = 0, info; info = infos[i++];) {
console.log('尊敬的客戶:');
info.apply(this, arguments);
console.log('已經(jīng)到貨了');
}
};
// 定義一個預(yù)訂者customerA,并指定預(yù)定信息
var customerA = function() {
console.log('黑色至尊版一臺');
};
// customerA 預(yù)定手機,并留下預(yù)約電話
merchants.listen('15888888888', customerA); // 預(yù)定成功
// 商家發(fā)布通知信息
merchants.publish('15888888888');
/**
尊敬的客戶:
黑色至尊版一臺
已經(jīng)到貨了
*/
取消訂閱
當(dāng)然,現(xiàn)實中我們可以預(yù)定,那么也可以取消預(yù)定。其實取消預(yù)定的方式也比較簡單,就是將客戶從預(yù)定列表中清除出去。代碼實現(xiàn)如下:
merchants.remove = function(id, fn) {
var infos = this.orderList[id];
if(!infos) return false;
if(!fn) {
infos && (infos.length = 0);
} else {
for(var i = 0, len = infos.length; i < len; i++) {
if(infos[i] === fn) {
infos.splice(i, 1);
}
}
}
};
merchants.remove('15888888888', customerA);
merchants.publish('15888888888'); // 您還沒有預(yù)訂信息!
全局的觀察者模式
實現(xiàn)的代碼結(jié)構(gòu)如下:
var observer = (function() {
var orderList = {},
listen,
publish,
remove;
listen = function(id, fn) {
...
};
publish = function() {
...
};
remove = function(id, fn) {
...
};
return {
listen: listen,
publish: publish,
remove: remove
}
})();
優(yōu)點:
使用了全局的觀察者模式后,我們不用管商家是誰,只要他能提供我們所需要的東西即可;而且我們也避免了為不同的商家都創(chuàng)建listen,publish,remove方法,這樣可以減少資源的浪費。
缺點:
使用全局的觀察者模式會明顯降低對象之間的聯(lián)系。一些方法將會被隱藏,而有時我們恰恰需要這些方法的暴露。
是先訂閱,還是先發(fā)布
在我被問到這個問題時,我也是一愣,當(dāng)時腦袋里就冒出了‘你怎么不問是先有雞,還是先有蛋’這樣的想法。
按照我的理解我們實現(xiàn)觀察者模式,都是訂閱者先訂閱,然后接收發(fā)布者的通知消息。沒有反過來想,發(fā)布者先發(fā)布一條消息,然后等訂閱者接收,因為在我的想象中,如果沒有訂閱者,這消息怎么成功發(fā)布。
后來有人跟我說有這樣的業(yè)務(wù)實現(xiàn),當(dāng)時我就不假思索的問什么業(yè)務(wù),他說QQ的離線模式。這種先發(fā)布后訂閱的形式是將信息先存儲起來,等到訂閱者訂閱,就立即將信息發(fā)送給訂閱者。如:當(dāng)我們將QQ調(diào)到離線模式,我們就無法接收信息;當(dāng)我們將QQ調(diào)到登錄模式,就馬上收在離線模式期間接收到的信息。
這樣的例子在生活中也有很多,還拿天氣預(yù)報,它也可以理解為是先發(fā)布,我們后訂閱的模式。天預(yù)報信息會發(fā)布在網(wǎng)上,存儲在各個服務(wù)器上,我們需要時打開手機就可以得到。
注:提到觀察者模式我們就不得不說一下推模型和拉模型。推模型在事件發(fā)生時,發(fā)布者會將變化狀態(tài)和數(shù)據(jù)都推送給訂閱者;拉模型在事件發(fā)生時,發(fā)布者只會給訂閱者一個狀態(tài)改變通知,訂閱者會根據(jù)發(fā)布者提供的接口主動拉取數(shù)據(jù)。