初步了解 发布-订阅模式

308 阅读9分钟

发布-订阅模式

  不论是在程序世界里还是现实生活中,发布-订阅模式应用都非常的广泛。先看一个现实中的例子。
  小明最近看上了一套房子,到了售楼处才被告知,该楼盘的房子早已告罄。好在售楼处的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 );

小结

  本张我们学习了发布-订阅模式,也就是常常说的观察者模式。发布-订阅模式非常的有用。   发布-订阅模式的优点非常明显,一为时间上的解偶,二为对象之间的解偶。他的应用非常的广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布-订阅模式还可以帮助实现一些别的设计模式,比如中介者模式。从架构上看,无论是MVC还是MVVC,都少不了发布-订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。   当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个 bug 不是件轻松的事情。