js设计模式——观察者模式

简介

​ 观察者模式又称为发布—订阅模式,主要定义的是对象间一对多的关系。如果在关系中一个对象改变的时候,其他依赖的对象也能够及时得到这个对象的通知。这个用法实际上在JavaScript中也很常见,比如在Vue里面,多个组件监听一个变量的变化,当变量的值发生变化的时候,这些依赖的对象也能够很快得到这个值的变化从而重新渲染数据。

一个简单的实例

我们先通过一个实例来说明这个观察者模式是怎样运行的:

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
var msgOffer = {};    //定义发布者
var msgGetter = function(name){ //定义接收者
this.name = name;
this.listen = (type,msg) => {
console.log(this.name + '收到的消息是:' + msg)
}
}
msgOffer.clientList = []; //定义收听者的回调函数
msgOffer.addClient = function(func){
this.clientList.push(func)
}
msgOffer.sendMess = function(){
var num = this.clientList.length;
for(let i = 0;i < num;i++){
func = this.clientList[i].listen;
//这里调用apply函数可以给箭头函数传入当前环境this
func.apply(this,arguments)
}
}
var stu1 = new msgGetter('小明');
var stu2 = new msgGetter('小红');
msgOffer.addClient(stu1);
msgOffer.addClient(stu2);
msgOffer.sendMess('good','放假了');

//执行的结果是
//小明收到的消息是:放假了
//小红收到的消息是:放假了

​ 在上面这段代码里,首先我们定义了一个用来发送消息的对象msgOffer,也定义了一个接收消息的对象msgGetter。这里用name区分不同的收听者,并定义了他们的监听回调函数(回调函数使用箭头函数定义的,虽然箭头函数没有自己的this,但是在之后我们调用回调函数的时候使用apply可以给箭头函数定义一个环境this指向被调用的实例化对象)。之后向msgOffer添加了几个方法,一个是用来增加收听者的数组clientList,一个是用来发布消息的函数sendMess。发布消息的函数通过每次遍历收听者数组并调用他们的listen函数来调用回调函数。这样的话整个发布—订阅模式定义完毕。

能够选择性监听的观察者模式

​ 上面的这段代码只是一个很简单的观察者模式,实际上他还是有很多设计上的问题。比如在上面,如果有的收听者只想接受特定的消息,比如在上述示例中,小明只想收听好消息,那么这个功能就需要加上去了。我们将代码改成下面这样:

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
var msgOffer = {};    //定义发布者
var msgGetter = function(name){
this.name = name;
}
msgOffer.clientList = []; //定义收听者的回调函数
msgOffer.addClient = function(obj,type,func){
let client = {
obj:obj,
type:type
}
obj.listen = func;
this.clientList.push(client)
}
msgOffer.sendMess = function(){
var type = Array.prototype.shift.call(arguments) //取出消息的类型
var num = this.clientList.length;
for(let i = 0;i < num;i++){
if(this.clientList[i].type == type){
func = this.clientList[i].obj.listen
var _that = this.clientList[i].obj;
func.apply(_that,arguments)
}
}
}
var stu1 = new msgGetter('小明');
var stu2 = new msgGetter('小红');
msgOffer.addClient(stu1,'good',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
msgOffer.addClient(stu2,'bad',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
msgOffer.sendMess('good','放假了');
msgOffer.sendMess('bad','交作业')

​ 执行上面的这行代码,可以发现小明和小红收到了各自想要接受的消息类型。上面的这段代码在原来的基础上,在发布者里面clientList数组用来存放多个对象,这些对象都有一个obj属性和一个type属性,用来存放收听者对象和想要获得的消息的类型。在每次收听者订阅的时候,都会声明他想要收听的消息的类型,并且会传入一个事件处理函数。这样的话,在发布者每次发布消息的时候,都会发布这个消息的名称和内容,匹配到想要收听该类消息的收听者后,就会执行对应的回调函数。这样就实现了收听者对于消息的过滤。

使代码更通用

​ 接下来我们做的事就是让这段代码更具有通用性。因为消息的发布者可能并不只是一个人,其他的对象也具有发布全体消息的功能,我们就对上面的代码进行一点修改使它更适用于更多的情况。

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
var eventer = {
clientList : [],
addClient: function(obj,type,func){
let client = {
obj:obj,
type:type
}
if(!obj.listen)
{
obj.listen = func;
this.clientList.push(client)
}
},
sendMess : function(){
var type = Array.prototype.shift.call(arguments) //取出消息的类型
var num = this.clientList.length;
for(let i = 0;i < num;i++){
if(this.clientList[i].type == type){
func = this.clientList[i].obj.listen
var _that = this.clientList[i].obj;
func.apply(_that,arguments)
}
}
}
}
var msgGetter = function(name){
this.name = name;
}
var insert = function(obj){
for(let i in eventer){
obj[i] = eventer[i]
}
}
var sendObj = {}
var sendObj2 = {}
insert(sendObj)
insert(sendObj2)
var stu1 = new msgGetter('小明');
var stu2 = new msgGetter('小红');
sendObj.addClient(stu1,'good',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
sendObj.addClient(stu2,'bad',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
sendObj2.addClient(stu1,'good',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
sendObj2.addClient(stu2,'bad',function(msg){
console.log(this.name + '收到的消息是:' + msg)
});
sendObj.sendMess('good','放假了');
sendObj2.sendMess('bad','交作业')

取消订阅

​ 有订阅就有取消订阅,当一个对象已经不需要依赖另一个对象的时候就需要取消对其的监听。代码如下所示:

1
2
3
4
5
6
7
8
9
10
remove : function(key,name){
let len = this.clientList.length;
for(let i = 0;i < len;i++){
//找到了对应的函数
if(this.clientList[i].type == key && this.clientList[i].obj.name == name){
this.clientList.splice(i,1)
break;
}
}
}

将事件全局化

​ 上述的代码已经基本实现了我们的要求,但是接下来我们想让这个时间的发布成为一个全局的事件,代码如下:

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
54
var eventer = (function(){
var clientList = [],addClient,sendMess,remove;
addClient = function(obj,type,func){
let client = {
obj:obj,
type:type
}
if(!obj.listen)
{
obj.listen = func;
this.clientList.push(client)
}
}
sendMess = function(){
var type = Array.prototype.shift.call(arguments) //取出消息的类型
var num = this.clientList.length;
for(let i = 0;i < num;i++){
if(this.clientList[i].type == type){
func = this.clientList[i].obj.listen
var _that = this.clientList[i].obj;
func.apply(_that,arguments)
}
}
}
remove = function(key,name){
let len = this.clientList.length;
for(let i = 0;i < len;i++){
//找到了对应的函数
if(this.clientList[i].type == key && this.clientList[i].obj.name == name){
this.clientList.splice(i,1)
break;
}
}
};
return{
clientList:clientList,
addClient:addClient,
sendMess:sendMess,
remove:remove
}
})()

var msgGetter = function(name){
this.name = name;
}
var stu1 = new msgGetter('小明');
var stu2 = new msgGetter('小红');
eventer.addClient(stu1,'good',function(msg){
console.log(this.name + '收到的消息是:' + msg)
})
eventer.addClient(stu2,'bad',function(msg){
console.log(this.name + '收到的消息是:' + msg)
})
eventer.sendMess('good','放假了')

离线通知

​ 当然,还有一种情况就是。当发布者发布消息的时候,可能收听者并不在线不能及时收听到。我们需要做的就是当用户上线的时候,消息能够传达到收听者。这个时候我们需要一个存放离线事件的堆栈,当事件发布的时候,如果这个时候还没有订阅者来订阅这个事件,我们就将发布事件的动作放在一个函数里,这些函数将会被存放在堆栈中,下一次有对象来访问的时候,就将他执行。

0%