发布-订阅模式
不论是在程序世界里还是现实生活中,发布-订阅模式应用都非常的广泛。先看一个现实中的例子。小明最近看上了一套房子,到了售楼处才被告知,该楼盘的房子早已告罄。好在售楼处的MM告诉小明,不久之后,还有一些尾盘推出,开发商正在办理相关的手续,手续办好后可以购买。到底什么时候,目前还没人知道。
于是小明记下了售楼处MM的电话,以后每天都会打电话过去问是否已经到了购买房子时间。除了小明,还有小红,小张,小李等人每天也会向售楼处咨询这个问题。一周后,售楼出的MM决定辞职,因为每天回答1000个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话留在售楼处。售楼MM答应他,新楼盘一推出就马上发信息通知小明。小红,小张等人也都一样。他们的号码被记在售楼处的花名册上,新楼盘推出的时候,售楼处MM会翻开花名册,遍历上面的号码,依次发短信给他们。
发布-订阅模式的作用
在刚刚的例子中,发送短信通知就是一个典型的发布-订阅模式。小明是订阅着者,他们订阅了房子的开售信息。售楼处作为发布者,会在合适的时候遍历花名册上的电话号码,依次给购房者发布消息。 可以发现,在这个例子中使用发布-订阅模式有着显而易见的有事。**** 购房者不用天天给售楼处打电话询问,在合适的时间点,售楼处作为发布者会通知这些消息的订阅者。
**** 购房者和售楼处不再强行耦合在一起,当有新的购房者出现,它只需要把手机号留在售楼处。售楼处不关心购房者的任何情况,不管购房者是男是女还是一只猴子。而售楼处的任何任何变动也不会影响购房者,比如售楼MM离职,售楼处从一楼搬到二楼,这些变化都跟购房者无关,只要售楼处记得发短信这件事。
第一点说明发布-订阅模式可以广泛应用与异步编程中,这是一种代替传递回调函数的方案。比如我们可以订阅ajax请求的success和error等事件。或者如果想在动画的每一帧完成之后做某一些事情,那我们可以订阅一个事件,然后在动画完成每一帧之后发布这个事件。异步编程中,使用发布-订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,只需要订阅感兴趣的事件发生点。
第二点说明发布-订阅模式可以取代对象之间的硬编码的通知机制,一个对象不再显示的调用另一个对象的某一个接口。发布-订阅模式让两个对象松耦合的联系在一起,虽然不太清楚彼此的细节,但这不影响他们之间的通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者者需要改变时,也不会影响之前的订阅者。只要之前约定的事件名没有变化,就可以自由的改变他们。
DOM事件
实际上,只要我们曾经在dom上绑定过监听事件,我们就曾用过发布-订阅模式,下面来看看这两句简单的代码发生了什么事情。 ``` document.body.addEventListener('click',function(){ console.log(1); },false) document.body.click() //模拟用户点击 ``` 在这里需要监控用户点击document.boy的动作,但是我们没办法预知用户在什么时候点击,所以我们订阅docuemnt.body的点击事件,当body被点击时,body节点便会向订阅者发布这个消息。这很像购房一样,购房者不知道什么时候开售,于是他在订阅消息后等待售楼处消息发布。当然我们还可以随意增加或者删除订阅者,增加订阅者不会影响发布者的代码。 ``` document.body.addEventListener('click',function(){ console.log(2); },false); document.body.addEventListener('click',function(){ console.log(3); },false); document.body.addEventListener('click',function(){ console.log(4); },false) document.body.click() //模拟用户点击 ```
自定义事件
除了DOM事件,我们还经常实现一些自定义的事件,这种依靠自定义事件完成的发布-订阅模式可以适用于任何javascript代码中。 现在一步步实现发布-订阅模式。 **** 首先指定谁是发布者。 **** 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者。 **** 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。另外,我们还可以往回调函数里添加一些参数,订阅者可以接收这些参数。这是很有必要的,比如售楼处可以在发给订阅者消息的短信里添加上房子的单价,面积等信息,订阅者收到这些消息后可以进行各自的处理。
var salesOffices = {};//定义售楼处
salesOffices.clientList = []; //定义缓存列表
salesOffices.listen = function(fn) { //往缓存列表里添加订阅者的回调函数
this.clientList.push(fn)
};
salesOffices.trigger = function(){
for(var i = 0,fn; i < this.clientList.length,fn = this.clientList[i];i ++) {
fn.apply(this,arguments) //执行订阅者的回调函数。
}
}
下面来进行简单的测试。
salesOffices.listen(function(price, square){ //小明订阅消息
console.log('价格:',price);
console.log('面积:',square);
});
salesOffices.listen(function(price,square) { //小红订阅消息
console.log('价格:',price);
console.log('面积',square)
})
salesOffices.trigger(20000,'200');//价格20000,面积200
salesOffices.trigger(30000,'250');//价格30000,面积250
至此,我们已经实现了一个最简单的发布-订阅模式,但是这里还存在一些问题.我们看到订阅者接收到了发布者的消息,但是小明只想买200平的房子,但是发布者把250平的房子也推送给了小明,这对小明来说是不必要的困扰。所以我们有必要添加一个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);
var fns = this.clientList[key];
if(!fns || fns.length == 0) {
return
}
for(var i = 0,fn; i < fns.length,fn = fns[i];i++) {
fn.apply(this,arguments);
}
}
salesOffices.listen('square200',function(price){ //小明订阅了200平的房子
console.log('小明关注了200平的房子,单价:'+ price)
})
salesOffices.listen('square250',function(price){ //小红订阅了250平的房子
console.log('小红关注了250房子,单价:'+price)
})
salesOffices.trigger('square200',20000)
salesOffices.trigger('square250',30000)
很显然,现在订阅者只订阅自己感兴趣的事了。
发布-订阅模式的通用实现
我们已经看到了如何让售楼处拥有发布-订阅模式。假设现在小明又去另一家售楼处买房子,那么这个代码是否必须在另一家售楼处对象上重写一次呢,有没有方法可以让所有的对象都拥有发布-订阅的功能呢。 答案是显然的,Javascript作为一门解释性执行语言,给对象动态的添加职责是理所当然的事。 所以我们要吧发布-订阅的功能提取出来,放在一个单独的对象内。var evnent = {
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);
var fns = this.clientList[key];
if(!fns || fns.length == 0) {
return false
}
for(var i = 0,fn;fn = fns[i++];){
fn.apply(this,arguments)
}
}
}
在定义一个installEvent函数,可以给所有的对象都添加发布-订阅的功能。
var installEvent = function(obj) {
for (var i in event) {
obj[i] = envent[i]
}
}
再来测试一番,我们给售楼处增加发布订阅的功能;
var salesOffices = {};
installEvent(salesOffices);
salesOffice.listen('square88',function(price){
console.log('小明订阅了88米的放在,单价:',price);
})
salesOffices.listen('square100',function(price){
console.log('小红订阅了100平的房子,单价:',price)
})
salesOffices.trigger('square88',10000);
saleaOffices.trigger('square100',20000);
取消订阅事件
有时候我们需要取消订阅事件的功能。比如小明突然不想买房子了,为了继续避免接到售楼处的电话,小明需要取消之前的订阅的事件。我们给event对象增加remove方法。event.remove = function(key,fn){
var fns = this.clientList[key];
if(!fns) { //如果key对应的消息没人订阅,则直接返回。
return false
}
if(!fn) { //如果没传fn,则表示key对应的消息全部删除
fns && fns.length = 0;
} else {
for (var y = fns.length - 1; y > 0; y-- ) { //反向遍历订阅的列表。
var _fn = fns[y]
if(fn == _fn) {
fns.splice(y,1)
}
}
}
}
var salesOffice = {};
var installEvent = function(obj) {
for(var i in event) {
obj[i] = evnet[i]
}
}
installEvent(obj);
var fn1 ,fn2
salesOffices.listen( 'squareMeter88', fn1 = function( price ){ console.log( '价格= ' + price );
});
salesOffices.listen( 'squareMeter88', fn2 = function( price ){ console.log( '价格= ' + price );
});
salesOffices.remove( 'squareMeter88', fn1 ); // 删除小明的订阅
// 小明订阅消息
// 小红订阅消息
salesOffices.trigger( 'squareMeter88', 2000000 );