發(fā)布-訂閱模式也叫觀察者模式,它定義對象之間一種一對多的依賴關系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知。在javaScript開發(fā)中,我們一般用事件模型來代替?zhèn)鹘y(tǒng)的發(fā)布-訂閱模式。
DOM的addEventListener就是發(fā)布-訂閱模式。
1,售樓消息訂閱
現(xiàn)實生活中,發(fā)布-訂閱的例子很多。比如買房,我們?nèi)ナ蹣翘幜粝挛⑿?,當樓房有新消息時,售樓小姐就把最新消息推送給我們。如果不用這種方式,我們就需要隔一段時間向售樓小姐發(fā)送消息詢問,這么一來,售樓小姐每天都要回復上千人的信息轟炸,心態(tài)爆炸。
簡單實現(xiàn)發(fā)布-訂閱:
- 首先確定誰是信息發(fā)布者(售樓處)
- 給發(fā)布者一個消息緩存列表,用于存放訂閱者的回調(diào)函數(shù),以便通知訂閱者(售樓處的花名冊)
- 發(fā)布消息的時候,發(fā)布者會遍歷緩存列表,依次觸發(fā)每個回調(diào)函數(shù)(遍歷花名冊,挨個發(fā)微信)
// js中的事件模型就是發(fā)布-訂閱模式,也叫觀察者模式,其實是1對p的問題,p指代對象,這就是個多p問題
var salesOffices = {} //定義售樓處
salesOffices.clientList = [] //緩存列表,存放訂閱者的回調(diào)函數(shù)
salesOffices.listen = function (fn) { //增加訂閱者
this.clientList.push(fn) //訂閱者的消息添加進緩存列表
}
salesOffices.trigger = function () {
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments) //arguments是發(fā)布消息時帶上的參數(shù)
}
}
//小明訂閱
salesOffices.listen(function (price, squareMeter) {
console.log('價格:' + price)
console.log('面積:' + squareMeter)
})
//小紅訂閱
salesOffices.listen(function (price, squareMeter) {
console.log('價格:' + price)
console.log('面積:' + squareMeter)
})
salesOffices.trigger(1000000, 100)
salesOffices.trigger(2000000, 90)
這是一個簡單的售樓消息發(fā)布-訂閱模式,但有一些問題,發(fā)布的時候小明和小紅都可以接收到所有人訂閱的消息,這很明顯是不合理的
2,修改代碼,讓訂閱者只收到自己的消息
增加一個用于標識的key,讓訂閱者只收到自己感興趣的消息
// 剛才那個沒有一一對應的關系,流程很不清晰,讓人懷疑里面有py交易
var salesOffices = {}
salesOffices.clientList = {}
salesOffices.listen = function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList[key].push(fn)
}
salesOffices.trigger = function () {
var key = Array.prototype.shift.call(arguments)
//去除該消息隊形的回調(diào)函數(shù)集合
fns = this.clientList[key]
//如果沒有訂閱該消息,則返回
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
//squareMeter88就是我們添加的key
salesOffices.listen('squareMeter88', function (price) {
console.log('價格:' + price)
})
salesOffices.listen('squareMeter140', function (price) {
console.log('價格:' + price)
})
salesOffices.trigger('squareMeter88', 1000000)
salesOffices.trigger('squareMeter140', 2000000)
3,發(fā)布-訂閱模式的通用實現(xiàn)
如果小明和小紅換了一家售樓部,那么又需要重新訂閱,這無疑是很麻煩的,我們需要實現(xiàn)一個通用的全局訂閱。
我們先將發(fā)布-訂閱的功能提取出來,放在一個單獨的對象中:
// 現(xiàn)在小明懷疑小紅還是有py交易,所以想換一個,做一個通用的觀察者
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)
fns = this.clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
}
然后在定義一個installEvent函數(shù),這個函數(shù)可以給所有對象都動態(tài)安裝發(fā)布-訂閱功能(實際就是做一個淺拷貝):
var installEvent = function (obj) {
for (var key in event) {
obj[key] = event[key]
}
}
現(xiàn)在我們來try一try剛才寫好的功能:
var salesOffices = {}
installEvent(salesOffices)
// 小明訂閱消息
salesOffices.listen('squareMeter88', function (price) {
console.log('價格:' + price)
})
salesOffices.listen('squareMeter140', function (price) {
console.log('價格:' + price)
})
//小紅訂閱消息
salesOffices.trigger('squareMeter88', 1000000)
salesOffices.trigger('squareMeter140', 2000000)
4,取消訂閱
有一天,小明不想奮斗了,小明的阿姨送了小明一棟樓,小明需要取消和售樓小伙的py交易:
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)
fns = this.clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
}
event.remove = function (key, fn) {
var fns = this.clientList[key]
if (!fns) {
return false
}
//不傳就是撤銷所有
if (!fn) {
fns && (fns.length=0)
}
else {
for (var i = 0, _fn; _fn = fns[i++];) {
if (_fn = fn) {
fns.splice(i, 1)
}
}
}
}
var installEvent = function (obj) {
for (var key in event) {
obj[key] = event[key]
}
}
var salesOffices = {}
installEvent(salesOffices)
salesOffices.listen('squareMeter88', f1 = function (price) {
console.log('價格:' + price)
})
salesOffices.listen('squareMeter140', function (price) {
console.log('價格:' + price)
})
// salesOffices.trigger('squareMeter88', 1000000)
salesOffices.remove('squareMeter88', f1)
salesOffices.trigger('squareMeter140', 2000000)
5,全局的發(fā)布-訂閱模式
之前實現(xiàn)的售樓消息訂閱,還存在兩點問題:
每個被做了淺拷貝的對象,都有所有的listen,trigger,和緩存列表clientList,這個有些浪費
小明和售樓處還有一些耦合。小明需要知道售樓小姐salesOffices才可以訂閱,如果小明又想知道salesOffices2的信息,那么又需要訂閱salesOffices2,這個操作比較麻煩。
我們可以使用一個中介,相當于賣方的中介公司,用來處理所有的訂閱請求。售樓處通過中介來發(fā)布消息,客戶通過中介來訂閱消息。
我們創(chuàng)建一個全局的Event對象,作為中介者:
//全局訂閱對象--中介統(tǒng)一管理
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)
fns = clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
remove = function (key, fn) {
var fns = clientList[key]
if (!fns) {
return false
}
//不傳就是撤銷所有
if (!fn) {
fns && fns.length
}
else {
for (var i = 0, _fn; _fn = fns[i++];) {
if (_fn = fn) {
fns.splice(i, 1)
}
}
}
}
return {
listen,
trigger,
remove
}
})()
Event.listen('squareMeter88', function (price) {
console.log('價格:' + price)
})
Event.trigger('squareMeter88', 1000000)
6,總結(jié)
發(fā)布-訂閱模式的有點很明顯,一是時間上的解耦,二是對象上的解耦。發(fā)布-訂閱模式的應用相當廣泛,它和中介者模式有一些類似之處,可以幫助實現(xiàn)中介者模式。
個人認為,在處理的事情上,發(fā)布-訂閱模式功能相對單一,主要就是收發(fā)消息,解決的是發(fā)布者和訂閱者之間的多對多關系;中介者模式功能五花八門,怎么寫都可以,解決的是對象與對象之間的多對多關系,將對象與對象的網(wǎng)狀關系,解耦成中介者與對象之間的一對多關系。