發(fā)布-訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知
1. 現(xiàn)實(shí)中的發(fā)布-訂閱模式
小明最近看上一套房子,到了售樓處之后才被告知歉摧,樓盤房子早已售空。好在售樓處告訴小明腔呜,不久后還有一些尾盤推出叁温,但到底是什么時候,目前還不知道核畴。
于是小明記下了售樓處的電話膝但,每天都會打電話詢問有沒有開盤。同樣谤草,除了小明跟束,還有小紅、小強(qiáng)也每天都會像售樓處打電話丑孩。一個星期后冀宴,售樓處電話已厭倦了每天的電話回答相同的內(nèi)容。
當(dāng)然温学,現(xiàn)實(shí)中顯然不是這樣的略贮,實(shí)際上是:售樓處會把意向的客戶的手機(jī)號碼留在售樓處,也就是小紅仗岖,小明逃延,小強(qiáng)的電話,當(dāng)開盤時轧拄,會依次發(fā)送短信來通知他們揽祥。
2. 發(fā)布-訂閱模式的作用
在剛剛的例子中,發(fā)短信通知就是一個典型的發(fā)布-訂閱模式檩电。小明拄丰,小紅他們都是訂閱者,他們訂閱了房子開盤的消息俐末。售樓處作為發(fā)布者料按,會在合適的時候依次發(fā)送給所有訂閱者發(fā)送消息。
優(yōu)點(diǎn):
- 購房者不用天天打電話給售樓處鹅搪,在合適的時間站绪,售樓處會作為發(fā)布者通知給所有的訂閱者
- 購房者和售樓處之前不再強(qiáng)耦合的在一起,當(dāng)有新的購房者出現(xiàn)時丽柿,只需要留下手機(jī)號碼恢准,售樓處不需要關(guān)系購房者的任何情況,只需要知道誰訂閱了購房消息即可甫题。
第一條說明發(fā)布訂閱模式可以廣泛引用于異步編程中馁筐,這是一種替代傳遞回調(diào)函數(shù)的方案。比如坠非,訂閱ajax請求的success敏沉,error事件。或者如果想在動畫的每一幀完成之后做一些事情盟迟,那我們可以訂閱一個事件秋泳,然后在動畫的每一幀完成之后去發(fā)布這個事件。在異步變成中使用發(fā)布-訂閱模式攒菠,我們就無序過多關(guān)注對象在異步運(yùn)行中的內(nèi)部狀態(tài)迫皱,而只需要訂閱感興趣的事件發(fā)生點(diǎn)。
第二條說明發(fā)布-訂閱模式可以取代對象之間硬編碼的通知機(jī)制辖众,一個對象不再顯試的嗲用另外一個對象的某個接口卓起。發(fā)布-訂閱模式讓兩個對象松耦合的聯(lián)系在一起,雖然不太清除彼此的細(xì)節(jié)凹炸,但不影響他們互相通信戏阅。當(dāng)有新的訂閱者出現(xiàn),發(fā)布者的代碼不需要做任何改變啤它,同樣發(fā)布者需要改變奕筐,訂閱者也不會影響。只要之前約定的事件名沒有變化蚕键。
3.DOM事件
實(shí)際上救欧,只要我們曾經(jīng)在DOM節(jié)點(diǎn)上綁定過事件函數(shù),那我們就曾經(jīng)使用過發(fā)布-訂閱模式锣光。
document.body.addEventListener('click', function(){
alert(1);
}, false);
document.body.click(); // 模擬用戶點(diǎn)擊
在這里我們需要監(jiān)控用戶點(diǎn)擊body的動作笆怠,但我們沒法預(yù)知用戶點(diǎn)擊的時間。所以我們訂閱了document.body上的click事件誊爹,當(dāng)body節(jié)點(diǎn)被點(diǎn)擊時蹬刷,body節(jié)點(diǎn)便會像訂閱者發(fā)布這個消息。
4.自定義事件
如何一步步實(shí)現(xiàn)發(fā)布-訂閱模式
- 首先制定好誰是發(fā)布者
- 然后給發(fā)布者添加一個緩存列表频丘,用于存放回調(diào)函數(shù)以便通知訂閱者办成。
- 最后發(fā)布消息的時候,發(fā)布者會遍歷這個緩存列表搂漠,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)迂卢。
另外,可以往回調(diào)函數(shù)里填入一些參數(shù)桐汤,訂閱者可以接受這些參數(shù)而克。這是很有必要的,比如售樓處可以在發(fā)給訂閱者消息里面添加房子面積怔毛,單價等信息员萍,訂閱者可以接受到消息做各自的處理
var salesOffices = {} // 定義售樓處
salesOffices.clientList = [] // 緩存列表,存放訂閱者的回調(diào)函數(shù)
salesOffices.listen = function (fn) { // 增加訂閱者
this.clientList.push(fn) // 訂閱的消息添加進(jìn)緩存列表
}
salesOffices.trigger = function () { // 發(fā)布消息
for (var i = 0; i < this.clientList.length; i++) {
var fn = this.clientList[i];
fn.apply(this, arguments) // arguments 是發(fā)布消息時帶上的參數(shù)
}
}
// 接下來進(jìn)行簡單的測試
salesOffices.listen(function (price, squareMeter) { // 小明
console.log('價格' + price)
console.log('面積' + squareMeter)
})
salesOffices.listen(function (price, squareMeter) { // 小紅
console.log('價格' + price)
console.log('面積' + squareMeter)
})
salesOffices.trigger(200000, 88)
salesOffices.trigger(300000, 99)
至此拣度,我們實(shí)現(xiàn)了一個最簡單的發(fā)布-訂閱模式碎绎,但是這里有一些問題螃壤,我們看到訂閱者接收到了發(fā)布者的每個消息,雖然小明只想買88平方米的房子筋帖,但是同時也接受到了99平方米的信息奸晴,所以我們需要標(biāo)識一個key,讓訂閱者制定粵自己感興趣的消息幕随,改寫后代碼如下:
var salesOffices = {} // 定義售樓處
salesOffices.clientList = {} // 緩存列表蚁滋,存放訂閱者的回調(diào)函數(shù)
salesOffices.listen = function (key, fn) { // 增加訂閱者
if (!this.clientList[key]){ // 如果還沒有訂閱過此類的消息,給該類消息創(chuàng)建一個緩存列表
this.clientList[key] = []
}
this.clientList[key].push(fn) // 訂閱的消息添加到緩存列表
}
salesOffices.trigger = function () { // 發(fā)布消息
var key = Array.prototype.shift.call(arguments) // 取出消息類型赘淮,也就是key
var fns = this.clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合
if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回
for (var i = 0; i < fns.length; i++) {
var fn = fns[i]
fn.apply(this, arguments)
}
}
salesOffices.listen('m88', function (price) {
console.log(price);
})
salesOffices.listen('m99', function (price) {
console.log(price);
})
salesOffices.trigger('m88', 200000)
salesOffices.trigger('m99', 300000)
很明顯,訂閱者可以只訂閱自己感興趣的事件了睦霎。
5.發(fā)布-訂閱模式的通用實(shí)現(xiàn)
小明又想去其他售樓處買房梢卸,那么就需要重新寫一個salesOffices,所以需要做如下改進(jìn)副女,把發(fā)布訂閱的功能提取出來蛤高,單獨(dú)放在一個對象內(nèi):
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) // 取出消息類型,也就是key
var fns = this.clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合
if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回
for (var i = 0; i < fns.length; i++) {
var fn = fns[i]
fn.apply(this, arguments)
}
}
}
// 定義一個installEvent函數(shù)碑幅,這個函數(shù)可以給所有對象都動態(tài)安裝發(fā)布-訂閱功能:
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i]
}
}
var salesOffices = {}
installEvent(salesOffices)
salesOffices.listen('ss88', function (price) {
console.log(price);
})
salesOffices.listen('ss99', function (price) {
console.log(price)
})
salesOffices.listen('ss88', 200000)
salesOffices.listen('ss99', 300000)
6. 取消訂閱的事件
有時候我們有這種需求戴陡,小明突然不想買房了,為了避免售樓處推送來的信息沟涨,可以取消之前訂閱的事件
event.remove = function (key, fn) {
var fns = this.clientList[key]
if (!fns) return false; // 如果key對應(yīng)得消息沒有被人訂閱恤批,則返回
if (!fn){
fns && (fns.length = 0) // 如果沒有傳入具體的回調(diào)函數(shù),表示需要取消key對應(yīng)消息的所有訂閱
}else {
for (var i = fns.length - 1; i > 0; i--) { // 反向遍歷訂閱的回調(diào)函數(shù)列表
var _fn = fns[i]
if (_fn === fn){
fns.splice(i, 1) // 刪除對應(yīng)得回調(diào)函數(shù)
}
}
}
}
var salesOffices = {}
installEvent(salesOffices)
salesOffices.listen('ss88', fn1 = function (price) {
console.log(price);
})
salesOffices.listen('ss99', fn2 = function (price) {
console.log(price)
})
salesOffices.remove('ss88', fn1)
salesOffices.trigger('ss88', 200000)
7.真實(shí)的例子--網(wǎng)站登錄
加入我們正在開發(fā)一個網(wǎng)站商場裹赴,網(wǎng)站里面有header喜庞,nav,購物車等各模塊棋返。這幾個模塊的渲染有一個共同的前提條件延都,就是必須先用ajax異步請求獲取用戶的信息。
至于ajax請求什么時候能成功返回用戶信息睛竣,這點(diǎn)我們沒法確定晰房。更重要的一點(diǎn)是,我們不知道除了購物車射沟,nav殊者,header之外,還有哪些地方需要用到用戶信息躏惋。如果他們和用戶之間信息產(chǎn)生了強(qiáng)耦合幽污,比如下面這樣的形式:
login.succ(function(data){
header.setAcatar(data.avatar) // 設(shè)置header模塊的頭像
nav.setAvatar(data.avatar) // 設(shè)置導(dǎo)航模塊的頭像
message.refresh() // 消息列表刷新
cart.refresh() // 購物車刷新
})
現(xiàn)在登錄模塊是我們負(fù)責(zé)編寫的,我們必須還了解header模塊設(shè)置頭像的方法叫setAvatar,購物車刷新的方法叫refresh.等等各種方法簿姨,如果后面需要加上收貨地址的模塊
login.succ(function(data){
header.setAcatar(data.avatar) // 設(shè)置header模塊的頭像
nav.setAvatar(data.avatar) // 設(shè)置導(dǎo)航模塊的頭像
message.refresh() // 消息列表刷新
cart.refresh() // 購物車刷新
adress.refresh() // 增加
})
這時候距误,又要重構(gòu)代碼簸搞。
當(dāng)我們用發(fā)布訂閱模式重寫后,對用戶感興趣的業(yè)務(wù)模塊將自定訂閱登錄成功的消息事件准潭。登錄成功后趁俊,登錄模塊只需要發(fā)布登錄信息,而業(yè)務(wù)方接受消息之后刑然,可以自己做各自的業(yè)務(wù)處理寺擂。
$.ajax('http://xxx.xx.com/login',function(data){
login.trigger('loginsuccess', data)
})
各模塊監(jiān)聽登錄成功的消息
var header = (function () {
login.listen('loginsuccess', function (data) {
header.setAvator(data.avator)
});
return {
setAvator: function(data) {
console.log('設(shè)置header的頭像')
}
}
})()
這時候,如果添加一個刷新收貨地址的行為泼掠,就可以讓收貨地址模塊開發(fā)人員去訂閱登錄成功這個事件
var adderss = (function () {
login.listen('loginsuccess', function (data) {
adderss.refresh(data)
});
return {
refresh: function (data) {
console.log('刷新地址')
}
}
})()
8. 全局的發(fā)布-訂閱對象
用一個全局的Event對象來實(shí)現(xiàn)怔软,訂閱者不需要了解消息來自哪個發(fā)布者,發(fā)布者也不需要消息會推送給訂閱者择镇,Event作為一個中介者的角色來吧發(fā)布者和訂閱者聯(lián)系到一起挡逼,代碼如下:
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
}
trigger = function() {
var key = Array.prototype.shift.call(arguments) // 取出消息類型,也就是key
var fns = clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合
if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回
for (var i = 0; i < fns.length; i++) {
var fn = fns[i]
fn.apply(this, arguments)
}
}
remove = function (key, fn) {
var fns = clientList[key]
if (!fns) return false; // 如果key對應(yīng)得消息沒有被人訂閱腻豌,則返回
if (!fn){
fns && (fns.length = 0) // 如果沒有傳入具體的回調(diào)函數(shù)家坎,表示需要取消key對應(yīng)消息的所有訂閱
}else {
for (var i = fns.length - 1; i > 0; i--) { // 反向遍歷訂閱的回調(diào)函數(shù)列表
var _fn = fns[i]
if (_fn === fn){
fns.splice(i, 1) // 刪除對應(yīng)得回調(diào)函數(shù)
}
}
}
}
return {
listen: listen,
trigger: trigger,
remove: remove
}
})()
Event.listen('ss88', function (data) {
console.log(data);
})
Event.trigger('ss88', 222)