# 发布—订阅模式
发布—订阅模式类似观察者模式,它定义了对象之间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。
# 理解发布—订阅模式
从字面上不难看出,发布—订阅模式有“发布”和“订阅”两个动词对应着发布者和订阅者两种对象,而订阅者往往是多个,所以也就是一对多的关系。当发布者达到某种状态时,将执行“发布”动作,订阅者们也将收到“发布”的消息。
在现实生活中,随处可见发布—订阅模式的例子。比如微信公众号,人们通过订阅喜欢的公众号来获取信息,当公众号管理者发布新文章时,微信后台将陆续通知订阅该公众号的所有读者。公众号管理者不需要自己主动通知所有的读者,只需要点击发布按钮即可,读者们也不需要一直盯着屏幕查看是否更新,因为公众号一旦更新,微信会立刻发出提醒通知。公众号管理者也不用担心新增的读者是否能收到更新消息,因为微信后台会将一切搞定。
由上面的例子可以得出以下两点:
- 发布—订阅模式使得我们无需关心一个对象内部状态的改变,只需要订阅感兴趣的某个事件发生点即可,即为时间上的解耦。
- 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不需要再显式地调用另一个对象的某个接口,即为对象之间的解耦。
# JavaScript 中的发布—订阅模式
在 JavaScript 中也能轻松找到发布—订阅模式的影子,例如 DOM 事件:
document.body.addEventListener('click', () => {
console.log('body')
})
因为无法预测用户动作,所以我们订阅 document.body
上的点击事件,当事件发生时,引擎会自动执行我们写入的回调函数。
# 实现发布—订阅模式
假如有这样一个需求,要求实现一个 Events
模块,可以实现自定义事件的订阅、触发、移除等功能。
使用该功能的代码如下:
const event = new Events()
const fn1 = (...args) => console.log('I want sleep1', ...args)
const fn2 = (...args) => console.log('I want sleep2', ...args)
event.on('sleep', fn1, 1, 2, 3)
event.on('sleep', fn2, 1, 2, 3)
event.fire('sleep', 4, 5, 6)
// I want sleep1 1 2 3 4 5 6
// I want sleep2 1 2 3 4 5 6
event.off('sleep', fn1)
event.fire('sleep', 4)
// I want sleep2 1 2 3 4
event.once('sleep', () => console.log('I want sleep too'))
event.fire('sleep')
// I want sleep2 1 2 3
// I want sleep too
event.fire('sleep')
// I want sleep2 1 2 3
使用发布—订阅模式实现该需求的代码如下:
class Events {
constructor() {
this.clients = {} // 存放事件类型以及对应的订阅者列表
}
// 静态方法,增加订阅者
static addListener(type, listener) {
if (!this.clients[type]) {
this.clients[type] = new Map()
}
const { origin, ...rest } = listener
this.clients[type].set(origin, rest)
}
// 订阅
on(type, fn, ...args) {
Events.addListener.call(this, type, {
origin: fn,
bindFn: fn.bind(this, ...args),
})
}
// 触发(发布)
fire(type, ...args) {
const clients = this.clients[type]
if (!clients || clients.size === 0) return false
for (const [origin, { bindFn, once }] of clients) {
bindFn.apply(null, args)
if (once) {
clients.delete(origin)
}
}
}
// 取消订阅
off(type, fn) {
const clients = this.clients[type]
if (!clients || clients.size === 0) return false
clients.delete(fn)
}
// 单次订阅(只触发一次)
once(type, fn, ...args) {
Events.addListener.call(this, type, {
origin: fn,
bindFn: fn.bind(this, ...args),
once: true,
})
}
}
通过以上例子可以发现,在 JavaScript 中主要采取了回调函数的形式来代替传统的发布—订阅模式,显得更加简单和优雅。其次,我们无需选择使用推模型还是拉模型,当事件发生时,通过 arguments
对象和 Function.prototype.apply()
方法可以很便捷地将更改的状态和数据都推送给订阅者。