前言:以前對發佈-訂閱模式沒有理解透,感受跟觀察者模式很是像,又看到有些文章說觀察者模式就是發佈-訂閱模式,搞的有點頭大。這篇文章以我的的理解對發佈-訂閱模式進行一次梳理,若是有錯誤或者不足的地方,但願你們不吝指出,共同進步!!!javascript
名字提供了兩個關鍵信息詞:發佈和訂閱,這是兩個行爲並分屬於兩個對象:發佈者和訂閱者。能夠用平常案例來解析這兩種行爲和對象,好比咱們做爲用戶來訂閱鬥魚的遊戲直播,有訂閱王者榮耀的有訂閱英雄聯盟的等等。前端
當各個遊戲有新的比賽時鬥魚會通知對應的訂閱觀衆,那這裏咱們用戶就是訂閱者,鬥魚是發佈者。用戶能夠訂閱本身感興趣的遊戲,鬥魚也能夠發佈不一樣的遊戲消息,收到消息通知的用戶能夠本身選擇是否觀看直播或者取消訂閱等行爲。java
因此從職責來上來說,訂閱者須要可以有訂閱的功能(包含取消訂閱),發佈者須要有發佈消息的功能。若是發佈者發佈的消息不是訂閱者訂閱的消息,那此訂閱者不用關心,好比鬥魚通知有新的英雄聯盟的賽事,那訂閱王者榮耀的觀衆就收不到此消息。程序員
固然一個訂閱者能夠同時訂閱多個事件,好比既訂閱英雄聯盟也訂閱王者榮耀,訂閱者也同時能夠對其餘發佈者進行訂閱,好比咱們還能夠訂閱掘金,有的用戶喜歡前端知識,有的用戶喜歡後端知識,也能夠訂閱微博等等,也就是說一個用戶能夠同時有不少的訂閱事件。後端
根據上面的分析,能夠看出訂閱者功能比較簡單,只要有訂閱和取消訂閱的功能基本就能夠了,先以訂閱者入手構建一個訂閱者的class:數組
//訂閱者構造器
class Subscribe {
constructor(name = 'subscriber') {
this.name = name
//隨機id模擬惟一
this.id = 'id-' + Date.now() + Math.ceil(Math.random() * 10000)
}
listen({
publisher,//訂閱的是哪一個發佈者
message,//訂閱的消息
handler//收到消息後的處理方法
}) {
//訂閱消息的回調函數
this[message + "_handler"] = handler
publisher && publisher.addListener(this, message)
return this
}
unlisten(publisher, message) {
publisher && publisher.removeListener(this, message)
return this
}
}
複製代碼
Subscribe代碼比較簡單有,兩個方法listen和unlisten,分別用來訂閱和取消訂閱,訂閱時要傳入訂閱的對象publisher,以及訂閱消息message和收到消息通知後的處理函數handler,先不用關心publisher.addListener,在下面建立Publish的class時候再說明。dom
取消訂閱時要傳入所訂閱的對象,以及訂閱的消息,這裏傳入消息參數時,就只解除對此消息的訂閱,若是不傳消息參數就解除對這個訂閱者全部消息的訂閱,實現邏輯也放在了下面的Publish裏面。函數
listen和unlisten都return this,這樣一個訂閱者實例就能夠以鏈式的方式執行連續訂閱或者取消訂閱的方法,下面的發佈者類裏面的publish方法也同樣能夠鏈式發佈消息。學習
先看代碼:測試
//發佈者構造器
class Publish {
constructor(name = 'publisher') {
this.messageMap = {} //消息事件訂閱者集合對象
//隨機id模擬惟一
this.id = 'id-' + Date.now() + Math.ceil(Math.random() * 10000)
this.name = name
}
addListener(subscriber, message) { //添加消息訂閱者
if (!subscriber || !message) return false
if (!this.messageMap[message]) { //若是消息列表不存在,就新建
this.messageMap[message] = []
}
const existIndex = this.messageMap[message].findIndex(exitSubscriber => exitSubscriber.id === subscriber.id)
if(existIndex === -1) {//不存在這個訂閱者時添加
this.messageMap[message].push(subscriber)
}else {//存在這個訂閱者時更新回調handler
this.messageMap[message][existIndex][message + "_handler"] = subscriber[message + "_handler"]
}
};
removeListener(subscriber, message) { //刪除消息訂閱者
if (!subscriber) return false
//若是傳了message只刪除此message下的訂閱關係,不然刪除此訂閱者的全部訂閱關係
const messages = message ? [message] : Object.keys(this.messageMap)
messages.forEach(message => {
const subscribers = this.messageMap[message];
if (!subscribers) return false;
let i = subscribers.length;
while (i--) {
if (subscribers[i].id === subscriber.id) {
subscribers.splice(i, 1)
}
}
if (!subscribers.length) delete this.messageMap[message]
})
};
publish(message, ...info) { //發佈通知
const subscribers = this.messageMap[message]
if (!subscribers || !subscribers.length) return this
subscribers.forEach(subscriber => subscriber[message + "_handler"](subscriber, ...info))
return this
};
};
複製代碼
發佈者主要功能也不復雜,就是在訂閱者訂閱消息的時候,執行addListener,把訂閱者存儲在自身的messageMap中,存儲的規則是以訂閱的消息做爲key,存儲結構關係以下:
messageMap = {
message1:[subscriber1,subscriber2,subscriber3,...],
message2:[subscriber1,subscriber2,subscriber3,...],
message3:[subscriber1,subscriber2,subscriber3,...],
...
}
複製代碼
當發佈者發佈消息時,遍歷對應的觀察者列表,執行各自的回調handler,發佈者在addListener添加訂閱者的時候,有兩個判斷須要注意下:
一、若是一個消息是第一次被訂閱,那就就以這個消息做爲key創建一個訂閱者列表;
二、若是一個訂閱者屢次訂閱一個消息,那就更新他的回調函數,以最後一次的爲最終回調。
發佈者在removeListener時,若是隻傳入了一個訂閱者,那就把這個訂閱者在此發佈者中全部消息的訂閱關係所有刪除,若是傳入了肯定的消息,那就只刪除對應的消息下的訂閱關係。當發佈者某一條消息下的訂閱者所有取消訂閱時,就delete掉髮布者的這個存儲關係,減小空數組。
直接上代碼:
//實例化發佈者juejin和douyu
const juejin = new Publish('juejin')
const douyu = new Publish('douyu')
//實例化訂閱者‘程序員A’和‘程序員B’
const programmerA = new Subscribe('programmerA')
const programmerB = new Subscribe('programmerB')
//訂閱者訂閱消息
//程序員A先訂閱了juejin的Javascript,若是推送的是關於closure,就比較感興趣。
//同時還訂閱了juejin的Java,若是推送的內容價錢超過15,就買不起
programmerA.listen({
publisher: juejin,
message: 'JavaScript',
handler: (self, info) => {
let { title, duration, price } = info
let result = `title[${title}]-> ${self.name} is not interested in it.`
if(title === 'closure') {
result = `title[${title}]-> ${self.name} is interested in it.`
}
console.log(`receive the message JavaScript from ${juejin.name}:`, result)
}
}).listen({
publisher: juejin,
message: 'Java',
handler: (self, info) => {
let { title, duration, price } = info
let result = `price[${price}]: ${self.name} can not afford it.`
if(price <= 15) {
result = `price[${price}]: ${self.name} can afford it.`
}
console.log(`receive the message JavaScript from ${juejin.name}:`, result)
}
})
//程序員B訂閱了douyu的英雄聯盟,表示很喜歡
//也訂閱了douyu的王者榮耀,也是很喜歡
//同時還訂閱了juejin的JavaScript,價錢小於10的,能支付的起
programmerB.listen({
publisher: douyu,
message: '英雄聯盟',
handler: (self, info) => {
let { title } = info
let result = `title[${title}]-> ${self.name} is interested in it.`
console.log(`receive the message 英雄聯盟 from ${douyu.name}:`, result)
}
}).listen({
publisher: juejin,
message: '王者榮耀',
handler: (self, info) => {
let { title } = info
let result = `title[${title}]-> ${self.name} is interested in it.`
console.log(`receive the message 英雄聯盟 from ${douyu.name}:`, result)
}
}).listen({
publisher: juejin,
message: 'JavaScript',
handler: (self, info) => {
let { title, duration, price } = info
let result = `price[${price}]: ${self.name} can not afford it.`
if(price <= 10) {
result = `price[${price}]: ${self.name} can afford it.`
}
console.log(`receive the message JavaScript from ${juejin.name}:`, result)
}
})
//juejin發佈消息通知
juejin.publish('JavaScript', {
title: 'prototype',
duration: 20,
price: 12
}).publish('JavaScript', {
title: 'closure',
duration: 15,
price: 8
}).publish('Java', {
title: 'interface',
duration: 18,
price: 10
})
//douyu發佈消息通知
douyu.publish('英雄聯盟', {
title: 'RNG VS SSW',
startTime: '2019-09-01 16:00',
}).publish('王者榮耀', {
title: 'KPL聯賽',
startTime: '2019-08-30 20:30',
})
//程序員B取消對douyu的訂閱,好好學習
programmerB.unlisten(douyu)
//發佈者再次發佈消息
juejin.publish('JavaScript', {
title: 'React',
duration: 20,
price: 25
}).publish('JavaScript', {
title: 'Vue',
duration: 15,
price: 20
})
douyu.publish('英雄聯盟', {
title: 'RNG VS SSW',
startTime: '2019-09-02 16:00',
}).publish('王者榮耀', {
title: 'KPL聯賽',
startTime: '2019-08-31 20:30',
})
複製代碼
發佈-訂閱模式是基於消息聯通的,必須在訂閱方和發佈方是同一個消息時纔會有執行結果。訂閱者只關注本身訂閱的消息,每一個訂閱者同時能夠訂閱多個發佈者對象。每一個發佈者在發佈消息時不用關心此消息是否有訂閱者,當發佈者發佈了被訂閱者訂閱的消息時,那麼訂閱者就根據消息詳情作出對應的處理。
訂閱者收到消息後具體怎麼作已經跟發佈者沒有關聯了,回調邏輯與發佈邏輯徹底解耦。訂閱者也能夠隨意訂閱本身感興趣的對象和消息,發佈-訂閱模式在邏輯上的主謂關係比較明確。