VueBloghyhero6

设计模式:发布订阅-模式(博客代码摘录JS设计模式代码一书)

2022-07-12 / 2022-07-12 / 335次浏览

不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常之广泛。我们先看一个

现实中的例子。

小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼 MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。 但到底是什么时候,目前还没有人能够知道。

于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除 了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决 定辞职,因为厌倦了每天回答 1000 个相同内容的电话。

当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在 了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一 样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册, 遍历上面的电话号码,依次发送一条短信来通知他们。

这个就是典型的现实版发布订阅模式,

第一点说明发布-订阅模式 可以广泛应用于异步编程中,这是一种替代传递回调函数的方案,比如,我们可以订阅 ajax 请求的 error、succ 等事件。 或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布-订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。

第二点说明发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用在显式地调用另外一个对象的某个接口。发布-订阅模式让两个对象松耦合地联系在一起,虽然不太了解彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

除了本身的DOM事件,是使用的发布和订阅模式,我们自己还会经常实现一些自定义事件,这种依靠自定义事件完成的发布-订阅模式可以用于 JavaScript 代码中。

实现发布订阅模式:

首先要指定好谁充当发布者:(比如售楼处)

然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册)

最后发布消息的时候,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)

另外我们有必要往回调函数中添加一些参数,订阅者可以接收这些参数。这是很有必要的,比如售楼处可以在发给订阅者的短信里加上房子的单价,面积,容积率等信息,订阅到这些消息之后可以各种进行处理。

var salesOffices = {}  // 定义售楼处

salesOffices.clientList = [] // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function ( fn ) { // 增加订阅者
    this.clientList.push( fn ) // 订阅的消息添加进缓存列表
}

salesOffices.trigger = function ( ) { // 发布消息
    for( var i = 0,fn; fn = this.clientList[ i++ ] ) {
        fn.apply(this, arguments) // (2) arguments 是发送消息时带上的参数
    }
}

// 进行一些简单的测试
salesOffices.listen(function (price, squareMeter) { // 小明的订阅消息
        console.log( '价格= ' + price )
        console.log( 'squareMeter= ' + squareMeter )
    })

salesOffices.trigger( 2000000, 88 )

但是这种订阅,问题在于小红,小明全量接收消息,也就是说,小明只想收到 88 平房子的消息,而发布者 把 110 平房子的消息也推送给了小明。所以需要增加一个key标识,让订阅者只订阅自己感兴趣的消息。改写之后的代码如下:

var salesOffices = {}

salesOffices.clientList = {} // 缓存列表,存放订阅者的回调函数 缓存对象由数组改成对象了

salesOffices.listen = function (key, fn) { // 增加订阅者
    if(!this.clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
       this.clientList[key] = []
    }
    this.clientList[key].push( fn ) // 订阅的消息添加进缓存列表, 现在有个关键字key 都可以装进key里
}

salesOffices.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)
    }
}
// 对这段代码进行一个小的测试
salesOffices.listen('squareMeter88', function(price){
    console.log( '价格= ' + price ); // 输出: 2000000
})

salesOffices.listen('squareMeter110', function(price){
    console.log( '价格= ' + price ); // 输出: 3000000
})

salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布 88 平方米房子的价格
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布 110 平方米房子的价格

现在这个订阅者实现了只订阅自己感兴趣的事情了。

这里需要理解下这段代码
[].shift.call(arguments) 这便是一个例子。
shift() 方法删除数组第一项,并删除返回项。
根据上边的理解,这句代码的意思就是:“删除并拿到arguments的第一项”
https://blog.csdn.net/Mrceel/article/details/96157800

好订阅者可订阅自己感兴趣的事情了

发布-订阅模式的通用实现
我们现在应该已经实现了让售楼处拥有接受订阅和发布事件的功能。假设小明又去另一个售楼处买房子,那么这段代码就必须在复制粘贴一次?能不能让JS 对象挂上一个方法我们通用的实现一些事情。例如,小明又换了一家售楼处去买房子,我们现在不想复制粘贴代码了。
JavaScript 作为一门解释性语言,给对象动态添加职责是理所当然的事情

所以我们把发布-订阅的功能提取出来,放在一个单独的对象内:

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),  // (1)
        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] = event[i]
    }
}
// 我们给售楼处对象salesOffices 动态增加发布-订阅功能:

var salesOffices = {}
installEvent(salesOffices)

salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅的消息
    console.log( '价格= ' + price );
});
salesOffices.listen( 'squareMeter100', function( price ){ // 小红订阅的消息
    console.log( '价格= ' + price );
})

上述代码,关键词,动态订阅

取消订阅事件
有时候,我们也许需要取消订阅事件功能,比如说小明突然不想买房子了,为了避免继续接收到售楼处推送来的短信,小明需要取消之前订阅的事件。现在我们给event对象增加remove方法

event.remove = function( key, fn ){
    var fns = this.clientList[ key ]
    if ( !fns ){ // 如果 key 对应的消息没有被人订阅,则直接返回 
        return false;
    }
    if ( !fn ){ // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
     fns && ( fns.length = 0 ) 
    } else {
        for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍历订阅的回调函数列表
            var _fn = fns[ l ]
            if ( _fn === fn ){
                fns.splice( l, 1 ) // 删除订阅者回调函数
            }    
        }
    }
}

最后,我们回归一下,发布订阅模式的主题。

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型 来替代传统的发布—订阅模式。