发布 - 订阅模式 (Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。
一、订阅模式的例子
比如当我们进入一个聊天室 / 群,如果有人在聊天室发言,那么这个聊天室里的所有人都会收到这个人的发言。这是一个典型的发布 - 订阅模式,当我们加入了这个群,相当于订阅了在这个聊天室发送的消息,当有新的消息产生,聊天室会负责将消息发布给所有聊天室的订阅者。
当我们去 adadis 买鞋,发现看中的款式已经售罄了,售货员告诉你不久后这个款式会进货,到时候打电话通知你。于是你留了个电话,离开了商场,当下周某个时候 adadis 进货了,售货员拿出小本本,给所有关注这个款式的人打电话。
这也是一个日常生活中的一个发布 - 订阅模式的实例,虽然不知道什么时候进货,但是我们可以登记号码之后等待售货员的电话,不用每天都打电话问鞋子的信息。
上面两个小栗子,都属于发布 - 订阅模式的实例,群成员 / 买家属于消息的订阅者,订阅消息的变化,聊天室 / 售货员属于消息的发布者,在合适的时机向群成员 / 小本本上的订阅者发布消息。
在这样的逻辑中,有以下几个特点:
- 买家(订阅者)只要声明对消息的一次订阅,就可以在未来的某个时候接受来自售货员(发布者)的消息,不用一直轮询消息的变化;
- 售货员(发布者)持有一个小本本(订阅者列表),对这个本本上记录的订阅者的情况并不关心,只需要在消息发生时挨个去通知小本本上的订阅者,当订阅者增加或减少时,只需要在小本本上增删记录即可;
- 将上面的逻辑升级一下,一个人可以加多个群,售货员也可以有多个小本本,当不同的群产生消息或者不款式的鞋进货了,发布者可以按照不同的名单 / 小本本分别去通知订阅了不同类型消息的订阅者,这里有个消息类型的概念;
二、程序实现
//发布者 const adadisPublish = { // 存储订阅者的一些属性信息 adadisBooks: {}, // 添加订阅,根据鞋子类型添加不同的客户订阅者信息 subscribShoe(type, customer) { if (this.adadisBooks[type]) { if (!this.adadisBooks[type].includes(customer)) { this.adadisBooks[type].push(customer); } } else { this.adadisBooks[type] = [customer]; } }, //取消订阅 unSubscribShoe(type, customer) { //取消存在的订阅者 if (!this.adadisBooks[type] || !this.adadisBooks[type].includes(customer)){ return; } const idx = this.adadisBooks[type].indexOf(customer); this.adadisBooks[type].splice(idx, 1); }, // 发布者发布消息 notify(type) { if(!this.adadisBooks[type]){ return; } this.adadisBooks[type].forEach(customer => { customer.update(type); }) } } //订阅者 const customer_one = { phoneNumer: "142xxxxx", update(type) { console.log(this.phoneNumer + ":订阅类型" + type + "updated") } } const customer_two = { phoneNumer: "158xxxxx", update(type) { console.log(this.phoneNumer + ":订阅类型" + type + "updated") } } //添加订阅 adadisPublish.subscribShoe("运动鞋", customer_one); adadisPublish.subscribShoe("运动鞋", customer_two); adadisPublish.subscribShoe("帆布鞋", customer_two); adadisPublish.notify("帆布鞋");
三. 发布 - 订阅模式的通用实现
我们可以把上面例子的几个核心概念提取一下,买家可以被认为是订阅者(Subscriber),售货员可以被认为是发布者(Publisher),售货员持有小本本(SubscriberMap),小本本上记录有买家订阅(subscribe)的不同鞋型(Type)的信息,当然也可以退订(unSubscribe),当鞋型有消息时售货员会给订阅了当前类型消息的订阅者发布(notify)消息。
主要有下面几个概念:
- Publisher :发布者,当消息发生时负责通知对应订阅者
- Subscriber :订阅者,当消息发生时被通知的对象
- SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组
- type :消息类型,订阅者可以订阅的不同消息类型
- subscribe :该方法为将订阅者添加到 SubscriberMap 中对应的数组中
- unSubscribe :该方法为在 SubscriberMap 中删除订阅者
- notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者
首先我们使用立即调用函数 IIFE(Immediately Invoked Function Expression) 方式来将不希望公开的 SubscriberMap
隐藏,然后可以将注册的订阅行为换为回调函数的形式,这样我们可以在消息通知时附带参数信息,在处理通知的时候也更灵活:
/通用实现 const Publish = (function () { const _subscribMap = {}; return { subscrib(type, subscriber) { if (_subscribMap[type]) { if (!_subscribMap[type].includes(subscriber)) { _subscribMap[type].push(subscriber) } } else { _subscribMap[type] = [subscriber]; } }, unSubscrib(type, subscriber) { if (!_subscribMap[type] || !_subscribMap[type].includes(subscriber)) { return; } const idx = _subscribMap[type].indexOf(subscriber); _subscribMap[type].splice(idx, 1); }, notify(type, ...payload) { if (!_subscribMap[type]) { return; } _subscribMap[type].forEach(subscriber => { subscriber(...payload); }) } } })() Publish.subscrib("运动鞋", (type, message) => console.log("type:" + type + ", message:" + message)) Publish.notify("运动鞋", "type_test", "message_test")
es6写法:
class Publisher { constructor() { this._subscribMap = {}; } subscrib(type, subscriber) { if (this._subscribMap[type]) { if (!this._subscribMap[type].includes(subscriber)) { this._subscribMap[type].push(subscriber) } } else { this._subscribMap[type] = [subscriber]; } } unSubscrib(type, subscriber) { if (!this._subscribMap[type] || !this._subscribMap[type].includes(subscriber)) { return; } const idx = this._subscribMap[type].indexOf(subscriber); this._subscribMap[type].splice(idx, 1); } notify(type, ...payload) { if (!this._subscribMap[type]) { return; } this._subscribMap[type].forEach(subscriber => { subscriber(...payload); }) } }