之前在看DMQ根據vue雙向數據綁定原理模擬實現了mvvm,里面有提高發(fā)布者-訂閱者模式,看了一些資料,今天自己簡單實現了一個發(fā)布-訂閱模式。
何為發(fā)布-訂閱模式?
其定義對象間一種一對多的依賴關系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知。
作了一幅畫,關于兩者的關系說明:

首次接觸這個概念的時候,會有幾個疑問,對象?指DOM對象還是自定義對象,還是兩者均可?依賴如何建立的?一個對象狀態(tài)的改變如何影響所有依賴它的對象?
這里面以微信公眾號為例,展開說明:
- 假如用戶A訂閱了 某一個公眾號G,那么當公眾號G推送消息的時候,用戶A就會收到相關的推送,點開可以查看推送的消息內容。
- 但是公眾號G并不關心訂閱的它的是男人、女人還是二哈,它只負責發(fā)布自己的主體,只要是訂閱公眾號的用戶均會收到該消息。
- 作為用戶A,不需要時刻打開手機查看公眾號G是否有推動消息,因為在公眾號推送消息的那一刻,用戶A就會收到相關推送。
- 當然了,用戶A如果不想繼續(xù)關注公眾號G,那么可以取消關注,取關以后,公眾號G再推送消息,A就無法收到了。
發(fā)布-訂閱模式抽象化
上面即是對發(fā)布-訂閱實例化的描述,但是跟上面問題的答案還是有些差距,我們付諸于代碼,以代碼的形式來模擬訂閱消息、發(fā)布消息、取消訂閱的功能,來解決上面提到的問題:
// 01-定義一個訂閱-發(fā)布模式函數;
function Pub2Sub() {
// 02-訂閱器;
this._observer = {}
}
// 03-原型對象上面添加方法;
Pub2Sub.prototype = {
constructor: Pub2Sub,
// 04-訂閱者;
subscribe: function (type, callback) {
if (Object.prototype.toString.call(callback) !== '[object Function]') return
// 訂閱器中是否存在訂閱行為;
if (!this._observer[type]) this._observer[type] = []
this._observer[type].push(callback)
return this
},
// 05-發(fā)布者;
publish: function () {
let _self = this
// 獲取發(fā)布行為
let type = Array.prototype.shift.call(arguments)
// 獲取發(fā)布主題
let theme = Array.prototype.slice.call(arguments)
// 獲取相關主題所有訂閱者
let subscribes = _self._observer[type]
// 發(fā)布主題
if (!subscribes || !subscribes.length) {
console.warn('unsubscribe action or no actions in observer, please check out')
return
}
subscribes.forEach(callback => {
callback.apply(_self, theme)
})
return _self
},
// 06-取消訂閱
unsubscrible: function (type, callback) {
if (!this._observer[type] || !this._observer[type].length) return
let subscribes = this._observer[type]
subscribes.some((item, index, arr) => {
if (item === callback) {
// 刪除對應的訂閱行為
arr.splice(index, 1)
return true
}
})
return this
}
}
// 實例化發(fā)布-訂閱模式
let ps = new Pub2Sub()
// 添加訂閱
let sub1 = function (data) {
console.log('sub1' + data)
}
let sub2 = function (data) {
console.log('sub2' + data)
}
ps.subscribe('click', sub1)
ps.subscribe('click', sub2)
// 實現發(fā)布、取訂及再發(fā)布
ps.publish('click', '第一次點擊消息').unsubscrible('click', sub2).publish('click', '第二次點擊消息')
// 打印結果依次是:
// sub1第一次點擊消息
// sub2第一次點擊消息
// sub1第二次點擊消息
上面代碼塊中,訂閱者1 sub1 和 訂閱者 sub2 分別訂閱了 'click',這個行為,當發(fā)布者 ps.publish 發(fā)布主題的時候,sub1 和 sub2 均收到了消息,在控制臺輸出 sub1第一次點擊消息 和 sub2第一次點擊消息,然后 訂閱者 sub2 又取訂了 click 行為,所以當 發(fā)布者 ps.publish 再次發(fā)布主題的時候,只有 sub1 才收到相關消息。
那么我們就通過代碼闡述了依賴是如何建立的,就是通過訂閱器來實現;
但是,上述實現的代碼存在兩個問題:
- 訂閱行為需要在發(fā)布行為之前,如果直接發(fā)布主題,訂閱器中沒有相關的訂閱行為,我這里手動拋出了警告。但是這是不應該的,正如用戶A訂閱了公眾號G,也可以查看G的歷史消息,所以這里需要實現查看發(fā)布主題歷史記錄的功能;
- 其次,上述功能的實現是通過定義在一個自定義對象,這樣就與發(fā)布-訂閱模式的松散耦合理念有些出入,所以還需要做到如何更優(yōu)雅的管理接口。
發(fā)布-訂閱模式優(yōu)化版
針對上述的問題,我在這個版本里面做了優(yōu)化,看代碼:
// 聲明一個全局發(fā)布-訂閱對象,為不同模塊之間的可能存在的通信做鋪墊
const Observer = (function () {
// 訂閱器
const _observer = {}
// 歷史記錄
const _cache = {},
_shift = Array.prototype.shift,
_slice = Array.prototype.slice,
_toString = Object.prototype.toString
// 訂閱
const subscribe = function (type, callback) {
if (_toString.call(callback) !== '[object Function]') return
// 訂閱器中是否存在訂閱行為;
if (!_observer[type]) _observer[type] = []
_observer[type].push(callback)
return this
}
// 發(fā)布
const publish = function () {
// 獲取發(fā)布行為
let type = _shift.call(arguments)
// 獲取發(fā)布主題
let theme = _slice.call(arguments)
// 記錄發(fā)布主題
if (!_cache[type]) {
_cache[type] = [theme]
} else {
_cache[type].push(theme)
}
// 獲取相關主題所有訂閱者行為
let subscribes = _observer[type]
// 發(fā)布主題
if (!subscribes || !subscribes.length) return
subscribes.forEach(callback => {
callback.apply(this, theme)
})
return this
}
// 取訂
const unsubscrible = function (type, callback) {
if (!_observer[type] || !_observer[type].length) return
let subscribes = _observer[type]
subscribes.some((item, index, arr) => {
if (item === callback) {
arr.splice(index, 1)
return true
}
})
return this
}
// 查看發(fā)布記錄
const viewLog = function (type, callback) {
if (!_cache[type] || _toString.call(callback) !== '[object Function]') return
_cache[type].forEach(item => {
callback.apply(this, item)
})
return this
}
return {
_observer,
_cache,
subscribe,
publish,
unsubscrible,
viewLog
}
}())
// 先發(fā)布主題;
Observer.publish('click', '第一次發(fā)布點擊消息')
Observer.publish('focus', '第一次發(fā)布聚焦消息')
Observer.publish('blur', '第一次發(fā)布失焦消息')
// 訂閱
let sub1 = function (data) {
console.log('sub1' + data)
}
let sub2 = function (data) {
console.log('sub2' + data)
}
let sub3 = function (data) {
console.log('sub3' + data)
}
Observer.subscribe('click', sub1)
Observer.subscribe('click', sub2)
Observer.subscribe('focus', sub3)
// 再發(fā)布、取訂、查看發(fā)布記錄
Observer.publish('click', '第二次發(fā)布點擊消息').unsubscrible('click', sub2).publish('click', '第三次發(fā)布點擊消息').publish('focus', '第二次發(fā)布聚焦消息').viewLog('click', function (message) {
console.log(message)
})
我們現在無論是先發(fā)布主題再訂閱,還是訂閱之后再發(fā)布主題,都不會有問題,因為在 Observer.publish 里面,發(fā)布者只關注自己發(fā)布主題功能,并且發(fā)布的時候將自己發(fā)布的對應主題保存。
在發(fā)布功能里面添加一個存放發(fā)布記錄的功能,在這里面我存放的是一個數組,是為了在 Observer.viewLog() 中方便調用。
通過一系列的發(fā)布、取訂、再發(fā)布、以及查看發(fā)布記錄,打印結果如下:
sub1第二次發(fā)布點擊消息
sub2第二次發(fā)布點擊消息
sub1第三次發(fā)布點擊消息
sub3第二次發(fā)布聚焦消息
// 這是查看歷史發(fā)布主題的結果,因為針對 click 行為,一共發(fā)布了三次主題
第一次發(fā)布點擊消息
第二次發(fā)布點擊消息
第三次發(fā)布點擊消息
理解對象間一對多的依賴關系
回到最初我們的問題,這個對象指的是既可以是自定義對象也可以是DOM對象
- 定義兩個模塊
let moduleA = {
// 偽代碼
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
let moduleB = {
// 偽代碼
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
// 下面是異步獲取到數據
// 偽代碼
ajax(function (data) {
// 發(fā)布數據,所有的訂閱均會拿到 data,然后按照自己的邏輯處理
Observer.publish(type, data)
})
可能會有人疑問,為什么需要這樣來傳遞數據,直接在 moduleA 和 moduleB 里面直接獲取數據不可以嗎?
答案肯定是可以的,但是發(fā)布-訂閱這種模式可以更優(yōu)雅地在不同模塊之間傳遞數據。
2019/02/09
const isFun = function (fun) {
return typeof fun === 'function'
}
class Observer {
constructor () {
this.messageCollector = {}
this.history = {}
}
on (...arg) {
const [type, callback] = arg
if (!isFun(callback)) {
throw new TypeError(`callback of arguments for function ${this.subscribe.name} must be a function `)
}
if (!this.messageCollector[type]) this.messageCollector[type] = []
this.messageCollector[type].push(callback)
return this
}
emit (...arg) {
const [type, ...theme] = arg
const subscribes = this.messageCollector[type]
if (!this.history[type]) {
this.history[type] = [theme]
} else {
this.history[type].push(theme)
}
for (const callback of subscribes) {
callback.apply(this, theme)
}
return this
}
off (...arg) {
const [type, callback] = arg
if (!this.messageCollector[type] || !this.messageCollector[type].length) return
if (!isFun(callback)) {
throw new TypeError(`callback of arguments for function ${this.subscribe.name} must be a function `)
}
const subscribes = this.messageCollector[type]
subscribes.some((item, index, arr) => {
if (item === callback) {
arr.splice(index, 1)
return true
}
})
return this
}
viewLog (...arg) {
const [type, callback] = arg
if (!this.history[type] || !isFun(callback)) return
const themes = this.history[type]
for (const theme of themes) {
callback.apply(this, theme)
}
return this
}
reset () {
this.messageCollector = {}
this.history = {}
return this
}
}
寫在最后
- 有人將觀察者模式和發(fā)布-訂閱模式認為是同一種模式,也有認為不是一種,仁者見仁,這里貼出一篇博客對兩者的介紹: 觀察者模式與發(fā)布/訂閱模式區(qū)別;
- 關于本人實現的發(fā)布-訂閱模式,仍存在問題,如果訂閱行為過多,在團隊協(xié)作中,會面臨著命名沖突的局面,我就拋磚引玉,貼出大牛對這塊邏輯的處理:JavaScript設計模式--觀察者模式;
- 最后再貼出DMQ對vue響應式原理的實現過程:mvvm,如果想深入了解vue原理,是一個不錯的過渡選擇。
- 關于發(fā)布-訂閱模式,在
ES6里面有了更好的實現,下次有時間的時候再繼續(xù)分享。 - 本文為原創(chuàng)文章,如果需要轉載,請注明出處,方便溯源,如有錯誤地方,可以在下方留言,歡迎??保创a已上傳到我的GitHub。