前言
上一章我们介绍了一种简单但是常见的设计模式,迭代器模式,也自己实现了一遍迭代器对象,这一章节要介绍的内容相信大家或多或少都耳濡目染过,那就是发布-订阅模式
正文
发布-订阅模式的定义
发布-订阅模式又称观察者模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖它的对象都会得到通知。前几章我们介绍的设计模式都分离了业务场景的特殊部分,那么发布-订阅模式呢,没错,就如名字所示的那样,发布-订阅模式分离了发布和订阅的功能,让两者间不再需要互相关注对方内部的逻辑
发布-订阅模式的实现
售楼处的例子
在实现发布-订阅模式前,我们模拟一个场景,假设有一个售楼处,每天会有不同的顾客来订阅楼盘的信息,而售楼的小姐姐会在楼盘的价格信息发生变化的时候,分别通知这些顾客,这就是一个明显的发布-订阅模式的例子。可以发现,这个例子体现了发布-订阅模式的两个明显的优点:
- 顾客不再需要主动一直向售楼小姐姐询问楼盘的消息,也不需要关注什么时候楼盘发生变化,售楼处会去监听,顾客只负责订阅这份消息
- 顾客和售楼处的耦合性降低,当有新的购房者出现的时候,只需要把手机号留在售楼处,而售楼处也并不在意用户的其他信息
实现售楼处的例子
在实现之前,我们先来缕缕这个场景发布-订阅实现的思路,无非就是下面三点:
- 先创建发布者对象(售楼处)
- 给发布者对象创建一个缓存数组,用来存放回调函数来通知不同的订阅者(顾客)
- 当需要发布消息的时候,发布者会遍历这个缓存数组,依次触发里面存放的订阅者回调函数
顺着上面的思路,我们不难封装出下面的售楼处:
// 定义售楼处
const saleOffices = function() {
this.clientList = []; // 缓存数组
this.listen = function(fn) { // 增加订阅者
this.clientList.push(fn); // 订阅的消息添加进缓存列表
}
this.trigger = function() { // 发布消息
for(let i = 0; i < this.clientList.length; i++) {
let fn = this.clientList[i];
fn.apply(this, arguments);
}
}
}
这样我们就已经实现好了一个简单的发布-订阅模式,但是上面的实现还并不是完美的,还存在一些问题,并没有对订阅定制化,这样会导致可能你没订阅这个消息也会发布消息,比如小陈只想订阅2号楼盘的价格变化,按上面的写法不管是什么楼盘都会去给小陈发布通知消息,所以我们需要加一个标志的key,让订阅者只收到他们感兴趣的内容
// 定义售楼处
const saleOffices = function() {
this.clientList = {}; // 缓存列表
this.listen = function(key, fn) { // 增加订阅者
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
}
this.trigger = function() { // 发布消息
// 这里使用arguments的原因是因为第一参数是key,后面可能还有一些发布的参数需要一起给订阅者
let key = Array.prototype.shift.call(arguments),
fns = this.clientList[key],
len = fns.length;
if (!fns || len === 0) {
return false;
}
for(let i = 0; i < len; i++) {
let fn = this.clientList[i];
fn.apply(this, arguments);
}
}
}
这样就好多了,顾客终于可以订阅自己感兴趣的部分了,售楼处也会根据用户的兴趣去发布对应的消息给顾客!
取消订阅的事件
假设有一天小陈又不感兴趣这个楼盘了,或者已经买好了,但是售楼处还是照常给小陈发短信,小陈想取消掉之前他订阅的事件应该怎么办呢,所以我们为售楼处额外再封装一个remove方法,用于订阅事件的取消
const saleOffices = function() {
this.clientList = {};
this.listen = function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
}
this.trigger = function() {
let key = Array.prototype.shift.call(arguments),
fns = this.clientList[key],
len = fns.length;
if (!fns || len === 0) {
return false;
}
for(let i = 0; i < len; i++) {
let fn = this.clientList[i];
fn.apply(this, arguments);
}
}
// 取消订阅的事件
this.remove = function (key, fn) {
let fns = this.clientList[key];
if (!fns) { // 如果key订阅的消息没有被人订阅,则直接返回
return false;
}
if (!fn) { // 如果传入的事件未定义,则认为是取消所有事件
fns && (fns.length = 0);
} else {
// 注意要倒序遍历,正序遍历删除节点会导致index错乱,导致后序的删除出错
for ( let i = fns.length - 1; i >= 0; i--) {
let _fns = fns[i];
if ( _fns = fns ) {
fns.splice(i, 1); // 删除订阅者的回调函数
}
}
}
}
javascript中实现发布-订阅模式的便利性
在java中实现发布-订阅模式,通常会把订阅者本身当作引用传入发布者对象中,同时,订阅者对象还需提供一个名为诸如update的方法,供发布者调用。而在javascript中我们可以直接使用回调函数来代替传统发布-订阅者模式,显得更加优雅
值得一提的是,vue中的双向绑定源码也是使用发布-订阅者模式去实现的,感兴趣的同学可以到网上搜相关源码去阅读
小结
这一章我们给大家介绍了发布-订阅模式,也称观察者模式,发布-订阅模式有着明显的优势:一、减少了程序时间上的耦合;二、减少了各模块间的耦合;可以应用于异步等场景,可以帮助我们编写更加松散耦合的代码
但是发布-订阅模式也有着它的缺陷,创建订阅者会消耗一定的内存,并且当消息一直未发布的时候,订阅者也会一直存在,对内存会有一定的损耗,并且大量的订阅者和发布者交错在一起,也会导致bug的难以追寻踪迹,难以判断其源头,所以也不能滥用发布-订阅模式
小伙伴们今天的学习就到这里了,如果觉得本文对你有帮助的话,欢迎转发,评论,收藏,点赞!!!
每天学习进步一点点,就是领先的开始。如果想继续提高,欢迎关注我,或者关注公众号”祯民讲前端“。大量前端技术文章,面试资料,技巧等助你更进一步!