前言
如果大家跟我一样,经常在掘金上看文章的话,就会发现掘金上很多作者对观察者模式和发布订阅模式是进行区分的。基于相信权威的学习法,我们可以看看相关书籍是如何定义观察者模式与发布订阅模式的。
曾探著的《JavaScript设计模式与开发实践》一书中对发布订阅模式有以下描述:
发布---订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
还有一本美国人写的《JavaScript设计模式》一书中对观察者模式有以下描述:
在事件驱动的环境中,比如浏览器这种持续寻求用户关注的环境中,观察者模式(又名发布者-订阅者(publisher-subscriber)模式)是一种管理人与其任务之间的关系(确切地讲,是对象及其行为和状态之间的关系)的得力工具。
设计模式不单是前端独有,后端同样有相关的设计模式实践。在一本由韩敬海主编讲解Java的设计模式的书籍:《设计模式(Java版)》中也有对观察者模式的描述:
观察者模式(Observer Pattern)也称发布订阅模式,它是一种在项目中经常使用的模式。
所以所谓观察者模式就是发布订阅模式,它们只是不同的叫法。
发布订阅模式的作用与实现
现实中的发布订阅模式------公众号例子
我们现实生活中就存在非常多的发布订阅模式,比如我们微信上的公众号就是一个典型的发布订阅模式。当我们关注了公众号(也就是订阅)后,公众号只要发布文章,我们就可以收到文章信息了。在这里公众号就是发布者 或者叫被观察者 ,关注公众号的人就是订阅者 或者叫观察者。
我们可以通过 JavaScript 代码进行抽象模拟公众号的策略行为。
定义发布者公众号对象以及实现公众号拥有的添加订阅者、广播信息、取消订阅的功能
javascript
// 定义发布者公众号
const weChatOfficialAccount = {
// 订阅公众号的人的记录列表
subscribers: [],
// 添加订阅者
addDep(fn) {
// 把订阅者添加进记录列表
this.subscribers.push(fn)
},
// 广播信息
notify(title) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(fn => fn(title));
},
// 取消订阅
remove(fn) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(fn)
// 删除订阅者
this.subscribers.splice(index, 1 )
}
}
上述代码模拟了公众号拥有一些功能,接下来我们进行订阅者的模拟测试。
javascript
// 订阅者小明
const subscriber = (title) => {
console.log('小明收到的公众号文章:', title)
}
// 小明关注公众号
weChatOfficialAccount.addDep(subscriber)
上述代码我们定义了一个订阅者小明(可以理解为函数 subscriber),小明关注公众号,其实就是调用公众号对象的 addDep 方法将订阅者小明添加到公众号的关注者记录列表中。
模拟发布者公众号发布文章:
javascript
// 发布者公众号发布文章,小明将收到文章信息
weChatOfficialAccount.notify('依赖追踪的基础 ------ 发布订阅模式')
上述代码将打印:小明收到的公众号文章:依赖追踪的基础 ------ 发布订阅模式。
模拟取消关注
javascript
// 小明取消关注公众号
weChatOfficialAccount.remove(subscriber)
// 公众号再次发布文章,小明不再收到文章信息
weChatOfficialAccount.notify('新文章:依赖追踪的基础 ------ 发布订阅模式')
我们可以看到当取消关注公众号后,公众号再次发布文章,订阅者小明将不再打印相关信息。
通过上述代码我们实现了一个最简单的发布订阅模式。但上述小明角色和行为还不够清晰,我们更改一下小明的角色代码,让其更语义化些。
javascript
// 订阅者小明
const subscriber = {
// 关注公众号
follow() {
weChatOfficialAccount.addDep(subscriber)
},
// 取消关注
unfollow() {
weChatOfficialAccount.remove(subscriber)
},
// 接受公众号推送信息
update(title) {
console.log('小明收到的公众号文章:', title)
}
}
同样地我们让公众号发布文章这个动作也更语义化些。
javascript
const weChatOfficialAccount = {
// 文章内容
article: '',
// 发布文章
setArticle(value) {
this.article = value
// 更新文章的时候通知所有的订阅者
this.notify()
},
// 广播信息
notify() {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(obj => obj.update(this.article))
},
// ...
}
我们更改好的公众号对象多了一个文章内容的属性:article 和发布文章的方法 setArticle(),在更新文章的时候去通知所有的订阅者,并且通过订阅者对象上的 update 方法告诉订阅者更新了什么文章内容。
接下来小明订阅公众号的动作只需要执行自己的 follow 方法即可。
小明订阅公众号
javascript
// 小明订阅公众号
subscriber.follow()
公众号发布文章
javascript
// 公众号发布文章
weChatOfficialAccount.setArticle('什么是发布订阅模式?')
将打印:小明收到的公众号文章:什么是发布订阅模式?。
小结
通过上述代码例子,我们就可以很好的印证了文章开头对 发布订阅模式 的定义,即:当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知 。我们从上述例子中可以看到当公众号对象的文章属性 article 的状态发生改变的时候,依赖它的对象 ------ 订阅者小明便得到了通知,打印了更新的文章内容。
熟悉 Vue 数据响应式原理的同学肯定感觉到很亲切,在数据响应式当中,一个响应式对象的属性状态发生改变的时候就会去通知所有依赖该属性的副作用函数进行重新执行。
现实中的发布订阅模式------求职者与企业
在以前网络没那么发达的年代,大家可能是通过报纸、线下公告栏去找工作的,那么很有可能去找的时候,该公司已经不需要人了,但在那个工作机会缺乏的年代,你可能会留下联系方式给招聘公司,并告诉他如果什么时候缺人了就告诉你。同样地也有很多其他求职者也在招聘公司留下了联系方式,当招聘公司缺人了,就会通过记录表中的名单一一进行通知。很明显这种求职者和招聘公司之间的关系也是一种发布订阅模式。
为了更具体清晰明了,我们通过面向对象的方式进行实现。
定义招聘公司
javascript
// 定义发布者招聘公司
class CompanyObservable {
constructor() {
// 岗位
this.post = ''
// 订阅招聘信息的人的记录列表
this.subscribers = []
}
// 发布岗位
setPost(value) {
this.post = value
// 岗位更新了,通知所有的订阅者
this.notify()
}
// 添加订阅者
addDep(sub) {
// 把订阅者添加进记录列表
this.subscribers.push(sub)
}
// 发布信息
notify() {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(sub => sub.update(this.post));
}
// 取消订阅
remove(sub) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(sub)
// 删除订阅者
this.subscribers.splice(index, 1 )
}
}
// 实例化发布者招聘公司
const company = new CompanyObservable()
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
}
// 订阅
follow() {
company.addDep(this)
}
// 取消订阅
unfollow() {
company.remove(this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到的招聘信息:${content}`)
}
}
我们通过面向对象的方式实现了一遍求职者与招聘公司之间的代码逻辑关系,我们可以看到跟上面公众号的例子是一样的。接下来我们进行相关的测试。
我们通过实例化订阅者类 Subscriber 创建一个求职者------郭靖,然后进行订阅招聘公司的岗位信息,招聘公司发布岗位信息的测试。
javascript
// 创建订阅者郭靖
const guojing = new Subscriber('郭靖')
// 郭靖订阅招聘公司的岗位信息
guojing.follow()
// 发布者公司发布岗位
company.setPost('前端工程师')
上面测试代码最终会打印:郭靖收到的招聘信息:前端工程师。
上述例子只是一个订阅者,我们可以创建多个订阅者以体现文章开头对发布订阅者模式的定义:一对多的依赖关系。
javascript
// 创建订阅者杨过
const yangguo = new Subscriber('杨过')
// 创建订阅者欧阳锋
const ouyangfeng = new Subscriber('欧阳锋')
// 杨过订阅招聘公司的岗位信息
yangguo.follow()
// 欧阳锋订阅招聘公司的岗位信息
ouyangfeng.follow()
// 发布者招聘公司发布岗位信息
company.setPost('后端工程师')
上面的测试代码最终会打印:
郭靖收到的招聘信息:前端工程师
郭靖收到的招聘信息:后端工程师
杨过收到的招聘信息:后端工程师
欧阳锋收到的招聘信息:后端工程师
招聘公司发现郭靖的信息作假,把他的信息删除了,这样郭靖将不再收到招聘公司新发布的信息。
javascript
// 郭靖信息作假,公司删除郭靖信息
company.remove(guojing)
// 公司再次发布招聘信息,郭靖不再收到招聘信息
company.setPost('运维工程师')
上面的测试代码最终打印:
杨过收到的招聘信息:运维工程师
欧阳锋收到的招聘信息:运维工程师
郭靖被删除了,所以不会打印郭靖的信息。
上面的实现还存在一个问题,如果郭靖只是一个前端工程师,那么他只希望接收前端工程师的岗位信息,但目前他能接收招聘公司所有发布的岗位信息,所以我们需要增加一个类型 key,让订阅者只订阅自己喜欢的内容。
重新改写后的公司代码如下:
javascript
// 定义一家公司
class CompanyObservable {
constructor() {
// 岗位信息
this.post = {
// 前端岗位信息
frontEnd: '',
// 后端的岗位信息
backEnd: '',
// 运维的岗位信息
DevOps: ''
}
// 订阅招聘信息的人的记录列表
this.subscribers = {}
}
// 发布岗位
setPost(event, value) {
this.post[event] = value
// 岗位更新了,通知所有的订阅者
this.notify(event)
}
// 添加订阅者
addDep(event, sub) {
// 如果还没有订阅过此类消息,则给该类消息创建一个记录列表
if (!this.subscribers[event]) {
this.subscribers[event] = []
}
// 把订阅者添加进记录列表
this.subscribers[event].push(sub)
}
// 发布信息
notify(event) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers[event].forEach(sub => sub.update(this.post[event]));
}
// 取消订阅
remove(event, sub) {
// 找到需要删除的订阅者
const index = this.subscribers[event].indexOf(sub)
// 删除订阅者
this.subscribers[event].splice(index, 1 )
}
}
// 实例化发布者招聘公司
const company = new CompanyObservable()
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
}
// 订阅,添加订阅事件类型参数
follow(event) {
company.addDep(event, this)
}
// 取消订阅
unfollow(event) {
company.remove(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到的招聘信息:${content}`)
}
}
修改后的测试代码如下:
javascript
// 创建订阅者郭靖
const guojing = new Subscriber('郭靖')
// 创建订阅者杨过
const yangguo = new Subscriber('杨过')
// 创建订阅者欧阳锋
const ouyangfeng = new Subscriber('欧阳锋')
// 郭靖只订阅前端的岗位信息
guojing.follow('frontEnd')
// 杨过只订阅后端的岗位信息
yangguo.follow('backEnd')
// 欧阳锋只订阅运维的岗位信息
ouyangfeng.follow('DevOps')
// 公司发布前端工程师岗位
company.setPost('frontEnd', '前端工程师')
// 公司发布后端工程师岗位
company.setPost('backEnd', '后端工程师')
// 公司发布运维工程师岗位
company.setPost('DevOps', '运维工程师')
上面测试代码最终会打印:
郭靖收到的招聘信息:前端工程师
杨过收到的招聘信息:后端工程师
欧阳锋收到的招聘信息:运维工程师
我们可以看到,修改后我们可以让订阅者只订阅自己喜欢的内容了。
小结
通过上面的例子我们可以看到通过多个订阅者的测试,我们已经验证了文章开头对发布订阅模式的定义:对象间一种一对多的依赖关系 。网上很多作者就把这种模式叫观察者模式 ,也就是一个被观察者,以及多个观察者。当被观察者发生改变的时候,所有的观察者都得到通知。从代码组织结构上看 CompanyObservable 类的实例对象是 被观察者,Subscriber 类的实例对象则是 观察者,CompanyObservable 和 Subscriber 之间是强耦合的。我们知道招聘公司是可以有多个的,而我们上述的代码组织方式,是没办法实现多个招聘公司进行发布信息的,也就是多个发布者和多个订阅者 进行联系。在现实生活中,招聘公司一般是通过招聘平台是发布招聘信息的,求职者则是通过招聘平台进行信息订阅,招聘公司一般是不会直接和求职者联系发送招聘信息的。
所以接下来我们要实现一个中介的角色,招聘平台,然后通过招聘平台把求职者和公司之间联系起来,而且可以有多个公司进行发布招聘信息和多个求职者订阅自己喜欢的信息,这个中介角色就是 事件总线 ,也叫 发布订阅中心。
事件总线(消息代理)的实现
如果对事件总线有了解的同学,会发现我们上面实现的发布者 CompanyObservable 类的实现跟事件总线的实现是很类似的。所以在代码层面我们只需要把每个发布者公共部分的功能代码进行抽离放在一个单独的对象中即可,比如:添加订阅者、广播通知(发射事件)、取消订阅等功能。代码如下:
javascript
// 定义事件总线
class EventBus {
constructor() {
// 订阅者的记录列表
this._events = {}
}
// 添加订阅者
on(event, sub) {
// 如果还没有订阅过此类消息,则给该类消息创建一个记录列表
if(!this._events[event]){
this._events[event] = []
}
// 把订阅者添加进记录列表
this._events[event].push(sub)
}
// 发射事件
emit(event, content) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this._events[event].forEach(sub => sub.update(content))
}
// 取消订阅
off(event, sub) {
// 找到需要删除的订阅者
const index = this._events[event].indexOf(sub)
// 删除订阅者
this._events[event].splice(index, 1 )
}
}
我们同时把相关的方法名称也修改成跟 Vue2 中的 事件总线 的方法名称一致,这样就非常方便大家理解了。同时我们也需要把发布者类 CompanyObservable 也进行修改,让发布者类更专注自己的职责。
javascript
// 实例化事件总线
const evtBus = new EventBus()
// 发布者招聘公司
class CompanyObservable {
constructor(companyName = '') {
// 公司名称
this._companyName = companyName
// 岗位信息
this.post = {
// 前端岗位信息
frontEnd: '',
// 后端的岗位信息
backEnd: '',
// 运维的岗位信息
DevOps: ''
}
}
// 发布岗位
setPost(event, value) {
this.post[event] = value
// 岗位更新了,通过中介通知所有的订阅者,并且把相关数据也传送给订阅者
evtBus.emit(event, { companyName: this._companyName, post: value })
}
}
我们可以看到修改后的发布者类 CompanyObservable 的职责更清晰了,只负责公司名称、岗位信息和发布岗位的功能,在发布岗位的时候调用事件总线 实例对象的 emit 方法通知所有的订阅者,并且把具体的公司名称和岗位名称信息也发送给了订阅者,这样订阅者收到发布者发布的信息就可以自行进行相关操作处理了。
同样地订阅者类型也需要进行相应的修改:
javascript
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
}
// 订阅,添加订阅事件类型参数
follow(event) {
evtBus.on(event, this)
}
// 取消订阅
unfollow(event) {
evtBus.off(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到${content.companyName}的招聘信息:${content.post}`)
}
}
订阅者类的修改主要是 update 方法,我们从发布者中拿到公司名称和岗位名称之后,我们就可以知道我们具体收到的招聘信息是哪个公司哪个岗位了。
接下来我们进行测试:
javascript
// 实例化发布者掘金公司
const juejin = new CompanyObservable('掘金')
// 实例化发布者阿里巴巴
const ali = new CompanyObservable('阿里巴巴')
// 实例化订阅者郭靖
const guojing = new Subscriber('郭靖')
// 实例化订阅者杨过
const yangguo = new Subscriber('杨过')
// 郭靖订阅前端的招聘信息
guojing.follow('frontEnd')
// 杨过订阅后端的招聘信息
yangguo.follow('backEnd')
// 掘金发布前端的岗位信息
juejin.setPost('frontEnd', '前端工程师')
// 阿里巴巴发布后端岗位信息
ali.setPost('backEnd', '后端工程师')
我们实例化两个发布者掘金和阿里巴巴,再实例化两个订阅者郭靖和杨过,然后让郭靖只订阅前端的招聘信息,杨过只订阅后端的招聘信息,然后掘金只发布前端的岗位信息,阿里巴巴只发布后端的岗位信息。上面的测试将打印:
郭靖收到掘金的招聘信息:前端工程师
杨过收到阿里巴巴的招聘信息:后端工程师
小结
我们可以看到公司与求职者之间不再存在强关联了,而是通过一个第三方的 事件总线 进行联系,并且通过事件总线可以实现多个发布者和多订阅者之间的联系,网上很多作者发把这一种代码组织结构模式称为发布订阅模式,而在我们最初的版本当中 CompanyObservable 类既是发布者也是发布订阅中心,也就是事件总线,很多作者就把这种在发布者对象上实现发布订阅中心的代码组织结构模式就称为观察者模式。那么发布者对象既可以是发布者又可以是发布订阅中心,那么它还可以是订阅者吗?很明显发布者对象既可以是发布者也可以是订阅者,所以我们不能只从代码组织结构去分辨模式,而是从意图进行分辨。不管上面的代码结构如何变化,它的核心都是管理对象间的依赖关系,或者说是事件间的依赖关系,一方变化了,所有跟其建立依赖关系的依赖都可以得到通知。
我们在上述小结中提出了,发布者与订阅者不是严格区分的,所以接下来我们就进行验证。
发布者与订阅者是严格区分的吗?
javascript
// 实例化事件总线
const evtBus = new EventBus()
// 发布者招聘公司
class CompanyObservable {
constructor(companyName = '') {
// 公司名称
this._companyName = companyName
// 岗位信息
this.post = {
// 前端岗位信息
frontEnd: '',
// 后端的岗位信息
backEnd: '',
// 运维的岗位信息
DevOps: ''
}
}
// 发布岗位
setPost(event, value) {
this.post[event] = value
// 岗位更新了,通过中介通知所有的订阅者,并且把相关数据也传送给订阅者
evtBus.emit(event, { companyName: this._companyName, post: value })
}
// 订阅,添加订阅事件类型参数
follow(event) {
evtBus.on(event, this)
}
// 取消订阅
unfollow(event) {
evtBus.off(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._companyName}收到${content.name ? content.name : ''}的招聘信息:${content.post}`)
}
}
我们在 CompanyObservable 类上也实现了订阅者的相关方法,这样 CompanyObservable 的实例对象也拥有了订阅者的功能了。
javascript
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
// 岗位信息
this.post = {
// 前端岗位信息
frontEnd: '',
// 后端的岗位信息
backEnd: '',
// 运维的岗位信息
DevOps: ''
}
}
// 发布岗位
setPost(event, value) {
this.post[event] = value
// 岗位更新了,通过中介通知所有的订阅者,并且把相关数据也传送给订阅者
evtBus.emit(event, { name: this._name, post: value })
}
// 订阅,添加订阅事件类型参数
follow(event) {
evtBus.on(event, this)
}
// 取消订阅
unfollow(event) {
evtBus.off(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到${content.companyName ? content.companyName : ''}的招聘信息:${content.post}`)
}
}
同样地我们也在 Subscriber 类上实现了发布者的相关功能,这样 Subscriber 类的实例对象也拥有了发布者的功能。
接下来我们进行测试
javascript
// 实例化发布者掘金公司
const juejin = new CompanyObservable('掘金')
// 实例化发布者阿里巴巴
const ali = new CompanyObservable('阿里巴巴')
// 实例化订阅者郭靖
const guojing = new Subscriber('郭靖')
// 实例化订阅者杨过
const yangguo = new Subscriber('杨过')
// 郭靖订阅前端的招聘信息
guojing.follow('frontEnd')
// 杨过订阅后端的招聘信息
yangguo.follow('backEnd')
// 掘金也订阅前端的招聘信息
juejin.follow('frontEnd')
// 阿里巴巴也订阅后端的招聘信息
ali.follow('backEnd')
// 掘金发布前端的岗位信息
juejin.setPost('frontEnd', '前端工程师')
// 阿里巴巴发布后端岗位信息
ali.setPost('backEnd', '后端工程师')
// 郭靖也发布后端岗位信息
guojing.setPost('backEnd', '后端工程师')
// 杨过也发布前端岗位信息
yangguo.setPost('frontEnd', '前端工程师')
在测试代码中,我们让本是发布者的掘金也进行订阅前端的招聘信息,让本是发布者的阿里巴巴也订阅后端的招聘信息,然后让本是订阅者的郭靖进行发布后端岗位信息,让本是订阅者的杨过也发布前端的岗位信息。最后我们发现打印了:

通过上述例子我们发现发布者既可以是发布者也可以是订阅者,也就是我们不能简单地从代码组织结构上去判断该对象是发布者还是订阅者。 这个就跟我们在 Vue 组件中使用事件总线一样,一个组件既可以通过 on 方法订阅事件,也可以通过 emit 方法发布事件,也就是一个组件既可以是订阅者也可以是发布者。
订阅者中介
我们目前的每一个订阅者所做的事情都是通过 Subscriber 类的 update 方法实现的,但目前没办法进行个性化的实现,比如说,郭靖要干的事情,肯定跟杨过要干的事情是不一样的,但目前他们要干的事情都一样。
我们上面把原来属于发布者的一些功能进行分离,抽象成一个事件总线的中介类,这样发布者就可以专注自己的功能实现。同样地每个订阅者也有自己不同的功能实现,所以我们也需要把订阅者也进行一个中介化。
其实实现很简单,我们只需要把每一个订阅者所做的事情,通过参数传进行就可以了,这样在调用订阅者的 update 方法的时候就去执行每个订阅者自己要实现的方法即可,这其实就是多态的实现。
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
那么我们通过订阅者中介就实现了,当发布者通知订阅者中介执行 update 方法的时候,就执行不同订阅者的所需要做的事情,在 Vue 应用中理解就是不同的组件。
代码修改如下:
javascript
class Subscriber {
constructor(fn) {
// 订阅者自己要干的事情,通过一个函数作为参数传进来
this._fn = fn
}
// 订阅,添加订阅事件类型参数
follow(event, fn) {
// 也可以在订阅的时候传递进来
if (fn) this._fn = fn
evtBus.on(event, this)
}
// 取消订阅
unfollow(event) {
evtBus.off(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
// 执行每个订阅者自己的方法
this._fn(content)
}
}
我们可以在实例化的时候传一个函数进来,也可以在订阅的时候传一个函数进来,然后保存起来,在将来得到通知的时候,就可以在 update 方法中进行调用。这样我们就可以实现不同订阅者的不同需求了。
以下便是测试代码:
javascript
// 实例化订阅者郭靖
const guojing = new Subscriber((content) => {
console.log(`我是郭靖,我收到${content.companyName ? content.companyName : ''}的招聘信息:${content.post}`)
})
// 实例化订阅者杨过
const yangguo = new Subscriber((content) => {
console.log(`我是杨过,我收到${content.companyName ? content.companyName : ''}的招聘信息:${content.post}`)
})
让每个订阅者所需要做的事情通过参数的形式传进来,这样更灵活,拓展性更强。
与 Vue 数据响应式的联系
经过上面的例子,我们知道发布订阅模式的核心就是管理对象间的依赖关系
我们上面讲公众号的例子的时候,weChatOfficialAccount 对象的 article 熟悉是通过 setArticle 方法进行手动修改的,从而让被观察者 weChatOfficialAccount 发生改变的,然后去执行观察者 subscriber 函数的。下面我们可以通过 Object.defineProperty 方法对 article 属性进行劫持监听,然后在 getter 的时候进行订阅,在 setter 的时候进行发布。
javascript
// 定义公众号
const weChatOfficialAccount = {
// 订阅公众号的人的记录列表
subscribers: [],
// 文章内容
article: '',
// 添加订阅者
addDep(fn) {
// 把订阅者添加进记录列表
this.subscribers.push(fn)
},
// 广播信息
notify(title) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(fn => fn(this.article))
},
// 取消订阅
remove(fn) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(fn)
// 删除订阅者
this.subscribers.splice(index, 1 )
}
}
defineReactive(weChatOfficialAccount, 'article', weChatOfficialAccount.article)
// 通过闭包把劫持的属性值进行缓存
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
get() {
// 存在订阅者就进行订阅操作
if (subscriber) weChatOfficialAccount.addDep(subscriber)
return val
},
set(newVal) {
val = newVal
// 通知所有的订阅者
weChatOfficialAccount.notify()
}
})
}
订阅者我们进行以下修改,接收的公众号文章不再通过传参的方式,而且直接读取公众号对象 weChatOfficialAccount 的属性 article。
javascript
// 订阅者小明
let subscriber = () => {
console.log(`收到的公众号文章:${weChatOfficialAccount.article}`)
}
// 读取一次,触发 getter 进行订阅
weChatOfficialAccount.article
// 设置为 null 防止重复订阅
subscriber = null
// 公众号发布文章则直接通过给属性赋值的方式
weChatOfficialAccount.article = '通过 Object.defineProperty 方法实现订阅发布模式'
上述测试代码的测试结果将会打印:
收到的公众号文章:通过 Object.defineProperty 方法实现订阅发布模式
上述例子并不是一个标准范式的发布订阅模式的代码结构,但它确实是运用到了发布订阅模式的核心原理,所以再次证明我们不能只从代码组织结构去分辨模式,而是从意图去分辨。
我们在本章当中只是蜻蜓点水般先讲解一些发布订阅模式在 Vue 数据响应式做的运用,作为一个引子,我们将在下一个章节进行详细讲解。
总结
本文章由浅入深地讲解了发布订阅模式,所谓发布订阅模式也叫观察者模式,有些作者会把一个观察者的对象上既实现观察者的功能也实现发布订阅中心的功能的代码组织结构就称之为:观察者模式,在这种情况下所谓发布者和订阅者是一对多的关系,也就是一个发布者和多个订阅者;而把发布订阅中心独立出来,让发布者和订阅者通过发布订阅中心进行通信联系,则有些作者把这一种代码组织结构则称之为发布订阅模式。而我们在相关权威的书籍中对这两种叫法都归为同一种模式,只是叫法不一样,所谓两种模式的本质都是不管代码结构如何变化,它的核心都是管理对象间的依赖关系,或者说是事件间的依赖关系,一方变化了,所有跟其建立依赖关系的依赖都将得到通知。同时发布者对象既可以是发布者也可以是订阅者,所以我们不能只从代码组织结构去分辨模式,而是从意图去分辨。
此外,我们上述的例子都是需要先进行订阅,然后再进行发布的,但在实际开发中,也可以先进行发布消息,然后把这条消息保存下来,等到有订阅者来订阅的时候,再把该消息发送给订阅者。所以理解一种模式不应该从代码组织结构去进行分辨,而是意图。
在理解了发布订阅模式之后,我们再去理解依靠依赖追踪的数据响应式的库或框架就会有种豁然开朗的感觉,原来如此简单。
上述文章写于:2023 年,由于个人原因今年 2026 年发布。
我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。