發佈-訂閱模式也叫觀察者模式,它定義對象之間一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。在javaScript開發中,我們一般用事件模型來代替傳統的發佈-訂閱模式。
DOM的addEventListener就是發佈-訂閱模式。
1,售樓消息訂閱
現實生活中,發佈-訂閱的例子很多。比如買房,我們去售樓處留下微信,當樓房有新消息時,售樓小姐就把最新消息推送給我們。如果不用這種方式,我們就需要隔一段時間向售樓小姐發送消息詢問,這麼一來,售樓小姐每天都要回覆上千人的信息轟炸,心態爆炸。
簡單實現發佈-訂閱:
- 首先確定誰是信息發佈者(售樓處)
- 給發佈者一個消息緩存列表,用於存放訂閱者的回調函數,以便通知訂閱者(售樓處的花名冊)
- 發佈消息的時候,發佈者會遍歷緩存列表,依次觸發每個回調函數(遍歷花名冊,挨個發微信)
// js中的事件模型就是發佈-訂閱模式,也叫觀察者模式,其實是1對p的問題,p指代對象,這就是個多p問題
var salesOffices = {} //定義售樓處
salesOffices.clientList = [] //緩存列表,存放訂閱者的回調函數
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是發佈消息時帶上的參數
}
}
//小明訂閱
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)
這是一個簡單的售樓消息發佈-訂閱模式,但有一些問題,發佈的時候小明和小紅都可以接收到所有人訂閱的消息,這很明顯是不合理的
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)
//去除該消息隊形的回調函數集合
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,發佈-訂閱模式的通用實現
如果小明和小紅換了一家售樓部,那麼又需要重新訂閱,這無疑是很麻煩的,我們需要實現一個通用的全局訂閱。
我們先將發佈-訂閱的功能提取出來,放在一個單獨的對象中:
// 現在小明懷疑小紅還是有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函數,這個函數可以給所有對象都動態安裝發佈-訂閱功能(實際就是做一個淺拷貝):
var installEvent = function (obj) {
for (var key in event) {
obj[key] = event[key]
}
}
現在我們來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
}
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,全局的發佈-訂閱模式
之前實現的售樓消息訂閱,還存在兩點問題:
每個被做了淺拷貝的對象,都有所有的listen,trigger,和緩存列表clientList,這個有些浪費
小明和售樓處還有一些耦合。小明需要知道售樓小姐salesOffices纔可以訂閱,如果小明又想知道salesOffices2的信息,那麼又需要訂閱salesOffices2,這個操作比較麻煩。
我們可以使用一箇中介,相當於賣方的中介公司,用來處理所有的訂閱請求。售樓處通過中介來發布消息,客戶通過中介來訂閱消息。
我們創建一個全局的Event對象,作爲中介者:
//全局訂閱對象--中介統一管理
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,總結
發佈-訂閱模式的有點很明顯,一是時間上的解耦,二是對象上的解耦。發佈-訂閱模式的應用相當廣泛,它和中介者模式有一些類似之處,可以幫助實現中介者模式。
個人認爲,在處理的事情上,發佈-訂閱模式功能相對單一,主要就是收發消息,解決的是發佈者和訂閱者之間的多對多關係;中介者模式功能五花八門,怎麼寫都可以,解決的是對象與對象之間的多對多關係,將對象與對象的網狀關係,解耦成中介者與對象之間的一對多關係。