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

首次接觸這個(gè)概念的時(shí)候,會(huì)有幾個(gè)疑問,對(duì)象?指DOM對(duì)象還是自定義對(duì)象,還是兩者均可?依賴如何建立的?一個(gè)對(duì)象狀態(tài)的改變?nèi)绾斡绊懰幸蕾囁膶?duì)象?
這里面以微信公眾號(hào)為例,展開說明:
- 假如用戶A訂閱了 某一個(gè)公眾號(hào)G,那么當(dāng)公眾號(hào)G推送消息的時(shí)候,用戶A就會(huì)收到相關(guān)的推送,點(diǎn)開可以查看推送的消息內(nèi)容。
- 但是公眾號(hào)G并不關(guān)心訂閱的它的是男人、女人還是二哈,它只負(fù)責(zé)發(fā)布自己的主體,只要是訂閱公眾號(hào)的用戶均會(huì)收到該消息。
- 作為用戶A,不需要時(shí)刻打開手機(jī)查看公眾號(hào)G是否有推動(dòng)消息,因?yàn)樵诠娞?hào)推送消息的那一刻,用戶A就會(huì)收到相關(guān)推送。
- 當(dāng)然了,用戶A如果不想繼續(xù)關(guān)注公眾號(hào)G,那么可以取消關(guān)注,取關(guān)以后,公眾號(hào)G再推送消息,A就無(wú)法收到了。
發(fā)布-訂閱模式抽象化
上面即是對(duì)發(fā)布-訂閱實(shí)例化的描述,但是跟上面問題的答案還是有些差距,我們付諸于代碼,以代碼的形式來(lái)模擬訂閱消息、發(fā)布消息、取消訂閱的功能,來(lái)解決上面提到的問題:
// 01-定義一個(gè)訂閱-發(fā)布模式函數(shù);
function Pub2Sub() {
// 02-訂閱器;
this._observer = {}
}
// 03-原型對(duì)象上面添加方法;
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)
// 獲取相關(guān)主題所有訂閱者
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) {
// 刪除對(duì)應(yīng)的訂閱行為
arr.splice(index, 1)
return true
}
})
return this
}
}
// 實(shí)例化發(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)
// 實(shí)現(xiàn)發(fā)布、取訂及再發(fā)布
ps.publish('click', '第一次點(diǎn)擊消息').unsubscrible('click', sub2).publish('click', '第二次點(diǎn)擊消息')
// 打印結(jié)果依次是:
// sub1第一次點(diǎn)擊消息
// sub2第一次點(diǎn)擊消息
// sub1第二次點(diǎn)擊消息
上面代碼塊中,訂閱者1 sub1 和 訂閱者 sub2 分別訂閱了 'click',這個(gè)行為,當(dāng)發(fā)布者 ps.publish 發(fā)布主題的時(shí)候,sub1 和 sub2 均收到了消息,在控制臺(tái)輸出 sub1第一次點(diǎn)擊消息 和 sub2第一次點(diǎn)擊消息,然后 訂閱者 sub2 又取訂了 click 行為,所以當(dāng) 發(fā)布者 ps.publish 再次發(fā)布主題的時(shí)候,只有 sub1 才收到相關(guān)消息。
那么我們就通過代碼闡述了依賴是如何建立的,就是通過訂閱器來(lái)實(shí)現(xiàn);
但是,上述實(shí)現(xiàn)的代碼存在兩個(gè)問題:
- 訂閱行為需要在發(fā)布行為之前,如果直接發(fā)布主題,訂閱器中沒有相關(guān)的訂閱行為,我這里手動(dòng)拋出了警告。但是這是不應(yīng)該的,正如用戶A訂閱了公眾號(hào)G,也可以查看G的歷史消息,所以這里需要實(shí)現(xiàn)查看發(fā)布主題歷史記錄的功能;
- 其次,上述功能的實(shí)現(xiàn)是通過定義在一個(gè)自定義對(duì)象,這樣就與發(fā)布-訂閱模式的松散耦合理念有些出入,所以還需要做到如何更優(yōu)雅的管理接口。
發(fā)布-訂閱模式優(yōu)化版
針對(duì)上述的問題,我在這個(gè)版本里面做了優(yōu)化,看代碼:
// 聲明一個(gè)全局發(fā)布-訂閱對(duì)象,為不同模塊之間的可能存在的通信做鋪墊
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)
}
// 獲取相關(guān)主題所有訂閱者行為
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ā)布點(diǎn)擊消息')
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ā)布點(diǎn)擊消息').unsubscrible('click', sub2).publish('click', '第三次發(fā)布點(diǎn)擊消息').publish('focus', '第二次發(fā)布聚焦消息').viewLog('click', function (message) {
console.log(message)
})
我們現(xiàn)在無(wú)論是先發(fā)布主題再訂閱,還是訂閱之后再發(fā)布主題,都不會(huì)有問題,因?yàn)樵?Observer.publish 里面,發(fā)布者只關(guān)注自己發(fā)布主題功能,并且發(fā)布的時(shí)候?qū)⒆约喊l(fā)布的對(duì)應(yīng)主題保存。
在發(fā)布功能里面添加一個(gè)存放發(fā)布記錄的功能,在這里面我存放的是一個(gè)數(shù)組,是為了在 Observer.viewLog() 中方便調(diào)用。
通過一系列的發(fā)布、取訂、再發(fā)布、以及查看發(fā)布記錄,打印結(jié)果如下:
sub1第二次發(fā)布點(diǎn)擊消息
sub2第二次發(fā)布點(diǎn)擊消息
sub1第三次發(fā)布點(diǎn)擊消息
sub3第二次發(fā)布聚焦消息
// 這是查看歷史發(fā)布主題的結(jié)果,因?yàn)獒槍?duì) click 行為,一共發(fā)布了三次主題
第一次發(fā)布點(diǎn)擊消息
第二次發(fā)布點(diǎn)擊消息
第三次發(fā)布點(diǎn)擊消息
理解對(duì)象間一對(duì)多的依賴關(guān)系
回到最初我們的問題,這個(gè)對(duì)象指的是既可以是自定義對(duì)象也可以是DOM對(duì)象
- 定義兩個(gè)模塊
let moduleA = {
// 偽代碼
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
let moduleB = {
// 偽代碼
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
// 下面是異步獲取到數(shù)據(jù)
// 偽代碼
ajax(function (data) {
// 發(fā)布數(shù)據(jù),所有的訂閱均會(huì)拿到 data,然后按照自己的邏輯處理
Observer.publish(type, data)
})
可能會(huì)有人疑問,為什么需要這樣來(lái)傳遞數(shù)據(jù),直接在 moduleA 和 moduleB 里面直接獲取數(shù)據(jù)不可以嗎?
答案肯定是可以的,但是發(fā)布-訂閱這種模式可以更優(yōu)雅地在不同模塊之間傳遞數(shù)據(jù)。
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ā)布-訂閱模式認(rèn)為是同一種模式,也有認(rèn)為不是一種,仁者見仁,這里貼出一篇博客對(duì)兩者的介紹: 觀察者模式與發(fā)布/訂閱模式區(qū)別;
- 關(guān)于本人實(shí)現(xiàn)的發(fā)布-訂閱模式,仍存在問題,如果訂閱行為過多,在團(tuán)隊(duì)協(xié)作中,會(huì)面臨著命名沖突的局面,我就拋磚引玉,貼出大牛對(duì)這塊邏輯的處理:JavaScript設(shè)計(jì)模式--觀察者模式;
- 最后再貼出DMQ對(duì)vue響應(yīng)式原理的實(shí)現(xiàn)過程:mvvm,如果想深入了解vue原理,是一個(gè)不錯(cuò)的過渡選擇。
- 關(guān)于發(fā)布-訂閱模式,在
ES6里面有了更好的實(shí)現(xiàn),下次有時(shí)間的時(shí)候再繼續(xù)分享。 - 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載,請(qǐng)注明出處,方便溯源,如有錯(cuò)誤地方,可以在下方留言,歡迎校勘,源碼已上傳到我的GitHub。