Fork me on GitHub

我所理解的发布订阅模式

神秘的设计模式

一直以来,我都感觉设计模式这个东西比较虚,总不知道是用来干嘛的。随着学习的深入,总听到别人说某某地方运用了什么设计模式,于是乎出于好奇就查资料加深了一下理解。目前也还只对发布订阅模式有些简单的理解。说起发布订阅模式,确实在很多地方都能看到它的影子,像JQ时代的trigger和on方法,还有现在的vue中的emit和on方法,之所以想先总结一些我目前所理解的发布订阅模式,是因为目前就这个设计模式了解的多一点,而且前段时间在学习vue双向绑定原理时发现也用到了发布订阅模式,但是不太理解那个里面的用法,所以,一步一步来吧。

发布订阅模式

我的理解就是订阅者把自己需要注册的事件名和回调统一注册到一个调度中心,然后当这个事件触发的时候,发布者就会发布到调度中心触发所有注册了这个事件的订阅者的回调。

我看过一个简单的比喻就是发布订阅模式就像我们以前像杂志社订杂志,你会告诉杂志社你需要什么杂志,并且杂志除了之后邮寄到哪里,而杂志社就相当于一个发布者,当最新一期的杂志出来之后他就会先查看某某杂志有哪些人订阅了,然后通知邮递员根据地址分发出去,这个根据不同人订阅时填的地址分发出去的过程实际上就可以理解为订阅者的回调。
image

实现一个简单的发布订阅模式

我们先用一个简单的代码来实现上面例子中的订阅杂志的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let crop = {};  //自定义一个对象
crop.list = {}; //存放缓存毁掉函数
crop.on = function(key, fn) {
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
}
crop.emit = function() {
let key = [].shift.call(arguments);
let fns = this.list[key];
if (!fns || !fns.length) {
return
}
fns.forEach( fn => {
fn.apply(this, arguments)
})
}
crop.on('readers', function(addr){
console.log('我订阅的《读者》更新了的话请寄到'+addr);
})
crop.on('youth', function(tel){
console.log('我订阅的《青年文摘》更新了的话请打我电话:'+tel);
})
crop.emit('readers', '浙江省杭州市xxx'); // 我订阅的《读者》更新了的话请寄到浙江省杭州市xxx
crop.emit('youth', '186xxxx7412'); //我订阅的《青年文摘》更新了的话请打我电话:186xxxx7412

最开始我看到简单的发布订阅模式的实现代码时不时太理解为什么一定要加一个key,其实用订阅杂志这个例子就很好理解了,你不告诉杂志社你订阅的杂志叫什么名字,那我怎么知道要邮寄哪本杂志给你呢,代码层面来说就会将所有订阅者的事件回调执行一次,那肯定不是我们需要的。

通用型实现

主要是增加了remove功能,简单理解就是我订阅了某本杂志,中途也是可以退订的嘛,哈哈,那样,就算那本杂志更新了,因为你已经不在通知列表里了,别人照样会通知,可是不会通知你了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 通用型
let event = {
list: {},
on: function(key, fn) {
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
emit: function() {
let key = [].shift.call(arguments);
let fns = this.list[key];
if (!fns || !fns.length) {
return
}
fns.forEach( (fn) => {
fn.apply(this, arguments);
})
},
remove: function(key, fn) {
let fns = this.list[key];
if (!fns || !fns.length) {
return
}
if (!fn) {
fns = []
}
else{
fns.forEach( (cb, i) => {
if (cb === fn) {
fns.splice(i, 1);
}
})
}
}
}
event.on('click', function(name){
console.log('你的名字是:'+name)
})
function cat() {
console.log('一起喵喵喵');
}
function dog() {
console.log('一起旺旺旺');
}
event.on('pet', cat);
event.on('pet', dog);
event.on('pet', data => {
console.log(data)
});
event.remove('pet', dog);
event.emit('click', 'hello world');
event.emit('pet', '二哈')

工作中的应用

就像前文讲的,JQ的trigger和on方法、vue中的emit和on方法,包括dom事件模型中的addEventListener和attachEvent等,实际上都是先订阅某个事件,并传入了触发事件的回调。当然还有一些其他用法,像用来做打点什么的,只是我还没实际用过就不多说了。特别提一下vue中的emit和on方法,这个我相信用过vue的都不会陌生,一般来说都是用他们做父子组件之间的通信,说白了就是在父组件中订阅一个自定义方法名和回调,然后在子组件中触发这个自定义事件和回调。

我想,既然这个父子组件的通信也利用的发布订阅模式,而且几个方法都是在vue的实例上的,所以在一些简单页面上做平行组件之间的通信也是可以通过vue实例上的emit和on方法实现的。事实上我尝试过,也是能做到的。只是后面又和同事讨论了一下,这样做平行组件之间通信的话太不稳定了,vue实例上注册的自定义事件容易冲突,而且状态管理也会很混乱,所以还是慎用吧,稍微大一点的项目还是得用vuex做组件之间通信的状态管理。

总结

其实将发布订阅模式的大体模式了解清楚之后确实能从很多地方看待他的影子,就我目前的理解来看,主要有这么几点好处吧:

  • 将订阅者和发布者给解耦了,作为发布者来说我不用管谁订阅了要干什么,我只要通知订阅了的对象并执行相应的回调就好了
  • 对于异步编程来说,可以更松耦合的代码编写

至于缺点吧,我查的资料来说主要是

  • 创建订阅者本身需要消耗一定的时间和内存
  • 多个发布者和订阅者嵌套在一起的时候,程序难以跟踪和维护

后面这一点我想大概就是我上文中讲到的处理vue非父子组件之间通信的情况吧,所以总结就是,模式虽好,不可以滥用噢,哈哈,不过正常使用的还是不会有什么的问题的。

而且深入理解这个模式也能够让我们更容易地理解一些框架和解构设计,我相信理解了并好好利用的话一定能对自己的编程能力带来不小的提升!

下一步,接着硬啃vue双向绑定和框架实现原理,加油!我相信那些一下子看不懂的东西多看几次多查一些资料,将内容拆分消化之后一定能慢慢看懂的!还有就是分享一点感悟,那就是贪多了嚼不烂,我发未知的现焦虑总是促使自己去不停地看新东西,可是由于时间和理解能力的关系往往又没有吃透,甚至有的时候出现了之前了解过查过资料的知识点又在查资料,这样时间的利用率其实是很低的。所以,以后遇到一个想要了解知识点就查资料尽可能地吸收一波之后总结、沉淀,至少要保证之后再遇到这个知识点不需要哦再去查过多的东西,看看自己写的总结和参考文章,那样应该也就回忆的差不多了。

所以,尽可能地深入,并坚持总结吧,加油!

本文同步发表在我的博客园:https://www.cnblogs.com/wancheng7/p/9575898.html

参考文章