第八章-发布订阅模式
定义:发布-订阅模式又称为观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。
# 现实中的发布-订阅模式
小明买房子,略......
# DOM 事件
如果我们曾经在 DOM 节点上绑定过事件函数,那么我们就一定使用过发布-订阅模式,来看看下面这两句简单的代码发生了什么事情:
document.body.addEventListener("click", function(){
console.log(2);
});
document.body.click(); // 模拟用户点击
2
3
4
在这里需要监控用户点击 document.body 的动作,但是我们没办法知道用户会在什么时候点击。所以我们订阅了 document.body 上的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息。
# 自定义事件
我们还是用小明买房的背景故事,来模拟当出现满足小明要求的房源后,中介会通知小明,示例代码如下:
var salesOffices = {}; // 售楼处
salesOffices.clientList = [];
salesOffices.listen = function (fn) {
this.clientList.push(fn);
};
salesOffices.trigger = function () {
for (var i = 0, fn; fn = this.clientList[i]; i++) {
fns.apply(this, arguments);
}
};
salesOffices.listen(function (squareMeter, price) {
console.log('面积' + squareMeter + '价格:' + price);
});
salesOffices.listen(function (squareMeter, price) {
console.log('面积' + squareMeter + '价格:' + price);
});
salesOffices.trigger(88, 2000000);
salesOffices.trigger(110, 5000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面已经实现了一个最简单的发布-订阅模式,但是这里面还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,小明只想买 88 平米的房子,发布者却把 100 平米的房子的信息也推送给了小明。所以我们有必要增加一个标识 key,让订阅者只订阅自己感兴趣的消息,改写后的代码如下:
var salesOffices = {}; // 售楼处
salesOffices.clientList = {};
salesOffices.listen = function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
};
salesOffices.trigger = function () {
var key = Array.prototype.shift.call(arguments);
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0; i < fns.length; i++) {
fns[i].apply(this, arguments);
}
};
salesOffices.listen('squareMeter88', function (price) {
console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
console.log('价格:' + price);
});
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000);
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 event = {
clientList: [],
listen: function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function () {
var key = Array.prototype.shift.call(arguments);
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布-订阅功能:
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i];
}
};
2
3
4
5
然后我们来测试一下,我们给售楼处对象动态的增加发布-订阅功能:
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen(
'squareMeter88',
(fn1 = function (price) {
console.log('价格:' + price);
})
);
salesOffices.listen('squareMeter88', function (price) {
console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
console.log('价格:' + price);
});
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 取消订阅的事件
有时候,我们也许需要取消已经订阅的事件的功能。比如小明突然不想买房子了,为了避免继续接到售楼的信息,小明需要将之前订阅的事件取消。现在我们给 event 对象增加 remove 方法:
remove: function (key, fn) {
var fns = this.clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var i = fns.length - 1; i >= 0; i--) {
var _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1);
}
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后再来测试一番
salesOffices.listen(
'squareMeter88',
(fn1 = function (price) {
console.log('价格:' + price);
})
);
salesOffices.listen('squareMeter88', function (price) {
console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
console.log('价格:' + price);
});
salesOffices.remove('squareMeter88', fn1);
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 全局的发布-订阅对象
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments);
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var i = fns.length - 1; i >= 0; i--) {
var _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1);
}
}
}
};
return {
listen,
trigger,
remove,
};
})();
Event.listen('squareMeter88', function (price) {
console.log('价格:' + price);
});
Event.trigger('squareMeter88', 2000000);
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
这样的话,调用者就不需要知道发布者是谁了,直接订阅对应的事件就可以了。
发布-订阅模式的问题,如果模块与模块之间用到了发布-订阅模式,那就是将一些逻辑隐藏在了背后,我们可能会搞不清楚这个消息是从哪个模块发送过来的,也不知道会有多少人订阅了这个消息,这样无疑会增加我们的维护成本,所以在实际的开发中,要结合真实的场景慎用发布-订阅模式。