零、目录
- 应用场景
- 实现原理
- 代码实现
- 全局模式下的订阅发布模式(泛化的订阅发布模式)
- 总结
一、应用场景
发布订阅模式,广泛的存在于在我们的生活之中。
举个一个简单的例子来说,当我们在浏览视频或者博客论坛之类的网站时,遇到感兴趣的up主或者博主, 我们通常会选择去订阅他们的频道或者内容。 这样一来,每当他们发布一个新的内容, 网站平台方就会通过某种渠道来通知我们, 我们便可以在第一时间了解到这一讯息, 至于是否选择第一时间阅读, 则却决于我们自己,而这就是一个典型的订阅发布模式。
反过来思考一下, 倘若不使用发布订阅模式, 当我们想了要解某一些特定信息时, 就需要自己定期的去访问信息平台,查看对方是否更新了新的内容。而这样一来,就会产生一些不必要的时间开支。(定期的访问信息源, 查看是否发生更新事件, 这种方式也称为 轮询 )
二、实现原理
在第一节应用场景介绍中, 我们可以从中抽离出两个角色 订阅者 subscriber(访问网站平台的用户), 发布者 publisher (发布讯息的用户), 他们之间的关系如图 2.1 所示
(2.1 一个基础的订阅发布者模型)
结合 图2.1 简单的来说, 发布者 会提供给所有用户一个公开的接口 subscribe,用户们可以通过这个接口提交一些注册信息(比如,一个将数据推送给自己的接口),进而将自己注册成为一个订阅者,同时 发布者 会维护一组数据, 用于保存一组可以联系 订阅者 的接口, 每当 发布者 发布新的内容时, 就可以将这一消息, 通过这组接口数据推送给所有订阅过该内容的 订阅者。(图中的 publish 接口, 实际就是通过遍历订阅者们队列来推送数据的一个方法)
三、代码实现
以下我们就用 JavaScript, 简单的模拟实现一下一个 发布订阅模式
/*
以下代码模拟场景:
用户们订阅了某个博主的博客, 并设置了自己想要的推送方式,当博主发布了博客,
将数据推送给订阅者们。
*/
"use strict"
function Obesever() {
let client = [], // 存储用户提供的联系接口
publish,
subscribe
publish = function(...args) {
for(let i = -1;client[++i];) {
client[i](args)
}
}
subscribe = function(fn) {
/**
* fn: 如何处理获取到的订阅内容
*/
return client.push(fn) // 返回订阅者下标, 用于后续退订需求
}
return {
publish,
subscribe
}
}
let remiliko = Obesever()
// A 用户订阅了
remiliko.subscribe(function(data) {
console.log('remiliko send', data, "to A")
})
// B 用户订阅了
remiliko.subscribe(function(data) {
console.log('remiliko send', data, "to B")
})
// remiliko 发布了一些数据, 然后通过接口将数据推送给订阅者
remiliko.publish("设计模式")
/* 输出结果 */
//remiliko send [ '设计模式' ] to A
//remiliko send [ '设计模式' ] to B
四、 全局模式下的发布订阅模式(泛化的发布定订阅模式)
在 第三节, 我们简单的实现了一个发布订阅模式, 但是代码存在了一定程度的 耦合(这些耦合在一些应用场景是可以被忽视的,比如对于 up主、博主 和 订阅者这种 单一映射 的需求模型之中,这种简单的发布订阅模型 就可以满足需求了),因为 发布者 本身,以及用户所订阅的事件都被约束在了固定的代码块里。
此时,我们不妨考虑以下这么一个应用场景:
用户会有一些需求,这些需求不再是针对于某一个特定对象(如关注某一个特定的用户), 而是想要获取满足一组规则的事物,在这一情况下,用户仅仅想要获取满足他们需求的讯息, 同时并不在乎这些讯息究竟是来自于哪一个用户。
更具体一点,比如用户有租房需求,某一个用户想要租价格在3000+以内一个月的房子(尽可能简化问题, 因为此处主要介绍发布订阅模式),而此时租房的app之中并没有搜索到满足他意愿的房源, 那么他希望能有一个订阅的功能, 每当出现新的满足 3000+一个月的房子讯息时, 就将这些信息推送给他。 对于这样一个需求而言, 如果维持 第三节 的模型, 那么用户可能需要通过订阅很多的房东来实现, 并且很有可能收到一些不符合需求的信息,从实际开发和生活中的角度来说,这是不合理的。
为了解决这个问题, 我们就需要引入一个新的角色比如 中间方, 由它来维护订阅者队列,至此 发布者 和 订阅者 之间并不再直接进行交互,而是通过平台方代为管理,整个执行流程也就从图 2.1 变为 图 4.1 所示。
(4.1 全局模式下的发布订阅发布 泛化 的数据流动图)
如 图4.1 所示, 我们从 发布者 publisher 身上将 订阅者队列 提取了出来,生成了一个 中间方 eventBus, 同时根据订阅的内容不同将订阅者队列进行了散列,这是为了实现更加多样化的 发布订阅者 模式,用户可以订阅想要的讯息, 而不再需要关注讯息来自于哪个用户, 而对于 发布者 来说只需要将他们想要推送的信息通过 eventBus 推送出去即可,而且也不再去需要维护一组订阅者数据了。
同样地, 我们也用 JavaScript 代码简单地实现一下
"use strict"
let eventBus = (function() {
let event = new Map(),
publish,
subscribe
subscribe = function(eventName, fn) {
/**
* eventName 订阅的事件名称
* fn: 对获取到的数据的处理函数
*/
let fns = event.get(eventName)
if(!fns) {
fns = []
fns.push(fn)
return event.set(eventName, fns) // 此处的 return 用于提前跳出函数调用栈
}
return fns.push(fn)
}
publish = function(...args) {
/* 由于发布的数据长度不确定, 因此利用 argument的特性 */
let eventName = args.shift(),
fns = event.get(eventName)
for(let i = -1;fns[++i];) {
fns[i](args)
}
}
return {
publish,
subscribe
}
})()
// 实际上是需要用一个命中函数来判别, 但为了简化问题, 不多处理
// 模拟客户A 订阅 3000以内的租房需求
eventBus.subscribe('3000', function(data) {
console.log(data)
})
// 模拟发布方A, 发送 3000以内
eventBus.publish('3000', "地铁站旁, 月2600 联系方式:xxxxxxxx")
// 模拟发布方B, 发送 3000以内
eventBus.publish('3000', "郊区, 月1600 联系方式:xxxxxxxx")
//[ '地铁站旁, 月2600 联系方式:xxxxxxxx' ]
//[ '郊区, 月1600 联系方式:xxxxxxxx' ]
在这种设计模式下,订阅方和 发布者 双方都是通过 eventBus 进行间接接触的, 这样的好处就是, 双方不必优先了解对方的详细信息, 而是直接将自己的需求(推送内容)推入 eventBus 这个管道, 让 evnetBus 去分发数据。
五、 总结
/* *个人总结, 并不具有代表性,只作分享开发心得* , 上述代码还有许多需要优化的点, 比如离线推送, 退订以及一些代码逻辑的优化, 但在此处主要是为了阐述模型实现, 并不多展开*/
发布订阅模式, 可以根据订阅载体的不同, 设置出两种不同风格的订阅发布方式。
- 具体的订阅对象(订阅方是具体的事物或者人),我们可以直接在订阅方的实例对象上去维护映射关系
- 抽象的订阅对象(订阅方是一组规则或者说是条件约束), 可以引入一个全局的事件管理器代为管理
发布订阅模式在JavaScript 开发中随处可见它的身影, 比如按钮点击事件, 以及Promise实现的底层代码等等, 熟练的掌握设计模式, 可以帮助我们更好的深入框架、底层代码的学习。