之前在看DMQ根据vue双向数据绑定原理模拟实现了mvvm,里面有提高发布者-订阅者模式,看了一些资料,今天自己简单实现了一个发布-订阅模式。
何为发布-订阅模式?
其定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
作了一幅画,关于两者的关系说明:
首次接触这个概念的时候,会有几个疑问,对象?指DOM对象还是自定义对象,还是两者均可?依赖如何建立的?一个对象状态的改变如何影响所有依赖它的对象?
这里面以微信公众号为例,展开说明:
- 假如用户A订阅了 某一个公众号G,那么当公众号G推送消息的时候,用户A就会收到相关的推送,点开可以查看推送的消息内容。
- 但是公众号G并不关心订阅的它的是男人、女人还是二哈,它只负责发布自己的主体,只要是订阅公众号的用户均会收到该消息。
- 作为用户A,不需要时刻打开手机查看公众号G是否有推动消息,因为在公众号推送消息的那一刻,用户A就会收到相关推送。
- 当然了,用户A如果不想继续关注公众号G,那么可以取消关注,取关以后,公众号G再推送消息,A就无法收到了。
发布-订阅模式抽象化
上面即是对发布-订阅实例化的描述,但是跟上面问题的答案还是有些差距,我们付诸于代码,以代码的形式来模拟订阅消息、发布消息、取消订阅的功能,来解决上面提到的问题:
// 01-定义一个订阅-发布模式函数;
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-发布者;
publish: function () {
let _self = this
// 获取发布行为
let type = Array.prototype.shift.call(arguments)
// 获取发布主题
let theme = Array.prototype.slice.call(arguments)
// 获取相关主题所有订阅者
let subscribes = _self._observer[type]
// 发布主题
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
}
}
// 实例化发布-订阅模式
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)
// 实现发布、取订及再发布
ps.publish('click', '第一次点击消息').unsubscrible('click', sub2).publish('click', '第二次点击消息')
// 打印结果依次是:
// sub1第一次点击消息
// sub2第一次点击消息
// sub1第二次点击消息
上面代码块中,订阅者1 sub1
和 订阅者 sub2
分别订阅了 'click',这个行为,当发布者 ps.publish
发布主题的时候,sub1
和 sub2
均收到了消息,在控制台输出 sub1第一次点击消息
和 sub2第一次点击消息
,然后 订阅者 sub2
又取订了 click
行为,所以当 发布者 ps.publish
再次发布主题的时候,只有 sub1
才收到相关消息。
那么我们就通过代码阐述了依赖是如何建立的,就是通过订阅器来实现;
但是,上述实现的代码存在两个问题:
- 订阅行为需要在发布行为之前,如果直接发布主题,订阅器中没有相关的订阅行为,我这里手动抛出了警告。但是这是不应该的,正如用户A订阅了公众号G,也可以查看G的历史消息,所以这里需要实现查看发布主题历史记录的功能;
- 其次,上述功能的实现是通过定义在一个自定义对象,这样就与发布-订阅模式的松散耦合理念有些出入,所以还需要做到如何更优雅的管理接口。
发布-订阅模式优化版
针对上述的问题,我在这个版本里面做了优化,看代码:
// 声明一个全局发布-订阅对象,为不同模块之间的可能存在的通信做铺垫
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
}
// 发布
const publish = function () {
// 获取发布行为
let type = _shift.call(arguments)
// 获取发布主题
let theme = _slice.call(arguments)
// 记录发布主题
if (!_cache[type]) {
_cache[type] = [theme]
} else {
_cache[type].push(theme)
}
// 获取相关主题所有订阅者行为
let subscribes = _observer[type]
// 发布主题
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
}
// 查看发布记录
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
}
}())
// 先发布主题;
Observer.publish('click', '第一次发布点击消息')
Observer.publish('focus', '第一次发布聚焦消息')
Observer.publish('blur', '第一次发布失焦消息')
// 订阅
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)
// 再发布、取订、查看发布记录
Observer.publish('click', '第二次发布点击消息').unsubscrible('click', sub2).publish('click', '第三次发布点击消息').publish('focus', '第二次发布聚焦消息').viewLog('click', function (message) {
console.log(message)
})
我们现在无论是先发布主题再订阅,还是订阅之后再发布主题,都不会有问题,因为在 Observer.publish
里面,发布者只关注自己发布主题功能,并且发布的时候将自己发布的对应主题保存。
在发布功能里面添加一个存放发布记录的功能,在这里面我存放的是一个数组,是为了在 Observer.viewLog()
中方便调用。
通过一系列的发布、取订、再发布、以及查看发布记录,打印结果如下:
sub1第二次发布点击消息
sub2第二次发布点击消息
sub1第三次发布点击消息
sub3第二次发布聚焦消息
// 这是查看历史发布主题的结果,因为针对 click 行为,一共发布了三次主题
第一次发布点击消息
第二次发布点击消息
第三次发布点击消息
理解对象间一对多的依赖关系
回到最初我们的问题,这个对象指的是既可以是自定义对象也可以是DOM对象
- 定义两个模块
let moduleA = {
// 伪代码
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
let moduleB = {
// 伪代码
todo() {
Observer.subscribe(type1, function (data) {
// 拿到 data 然后做一些事情
})
}
}
// 下面是异步获取到数据
// 伪代码
ajax(function (data) {
// 发布数据,所有的订阅均会拿到 data,然后按照自己的逻辑处理
Observer.publish(type, data)
})
可能会有人疑问,为什么需要这样来传递数据,直接在 moduleA
和 moduleB
里面直接获取数据不可以吗?
答案肯定是可以的,但是发布-订阅这种模式可以更优雅地在不同模块之间传递数据。
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
}
}
写在最后
- 有人将观察者模式和发布-订阅模式认为是同一种模式,也有认为不是一种,仁者见仁,这里贴出一篇博客对两者的介绍: 观察者模式与发布/订阅模式区别;
- 关于本人实现的发布-订阅模式,仍存在问题,如果订阅行为过多,在团队协作中,会面临着命名冲突的局面,我就抛砖引玉,贴出大牛对这块逻辑的处理:JavaScript设计模式--观察者模式;
- 最后再贴出DMQ对vue响应式原理的实现过程:mvvm,如果想深入了解vue原理,是一个不错的过渡选择。
- 关于发布-订阅模式,在
ES6
里面有了更好的实现,下次有时间的时候再继续分享。 - 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎校勘,源码已上传到我的GitHub。