🔥如何在函数式编程中使用设计模式-发布订阅模式

40 阅读5分钟

定义

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。

实际上,DOM节点的绑定事件函数,就是一个标准的发布订阅模式,比如:

document.body.addEventListener( 'click', function(){
alert(2);
}, false );
document.body.click(); // 模拟用户点击

在这里面addEventListener是订阅者,而click是发布者。在这里需要监控点击的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅了一个click事件,当body节点被点击时,body便会向所有订阅者发布这个消息。

原理

那我们要如何一步步实现发布-订阅模式呢

  • 首先要确定发布者
  • 其次给发布者确定缓存列表,用于存放回调函数以便通知订阅者
  • 最后就是内部触发机制,发布消息时,发布者需要遍历这个缓存列表,依次触发里面存放的订阅者回调函数

实现

接下来我们来实现一个简单的发布订阅的例子 假设小明要到餐厅吃饭,但是餐厅已经没有位子了,这时候需要使用发布-订阅模式,当有位子时告知小明座位的位置号以及座位信息。

const 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( number, people ){ // 小明订阅消息
 console.log( '号码: ' + number );
 console.log( '最大人数= ' + people );
});
salesOffices.listen( function( number, people ){ // 小红订阅消息
 console.log( '号码: ' + number );
 console.log( '最大人数= ' + people );
});
salesOffices.trigger( 10, 2 ); 
salesOffices.trigger( 12, 4 ); 

改进

上面我们已经完成了最简单的发布订阅模式的实践。但是这里还存在一些改进的空间。我们看到订阅者接收到了发布者发布的每个消息,小明只想座靠墙的单人座,但依然接收到了两座的消息,这对小明来说是不必要的困扰。我们可以增加一个标识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(...args){ // 发布消息
 var key = args.shift(), // 取出消息类型
 fns = this.clientList[ key ]; // 取出该消息对应的回调函数集合
 if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
     return false;
 }
 for( var i = 0, fn; fn = fns[ i++ ]; ){
     fn.apply( this, args ); // (2) // arguments 是发布消息时附送的参数
 }
};
salesOffices.listen( '单人座', function( number ){ 
 console.log( '号码 ' , number ); 
});
salesOffices.listen( '四人座', function( number ){ 
 console.log( '号码' ,number ); 
});
salesOffices.trigger( '单人座',10);
salesOffices.trigger( '四人座', 12);

这样,订阅者可以只订阅自己感兴趣的事件了。

通用实现

现在我们已经看到了如何让餐厅有接受订阅和发布事件的功能。假设现在小明又去另一个餐厅吃饭,那么这段代码是否必须在另一个餐厅重写呢?有没有办法可以让所有餐厅使用统一的发布-订阅功能呢?

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

const event = {
    clientList: {},
    listen(key,fn){
        if(!this.clientList[key]){
            this.clientList[key] = []
        }
        this.clientList[key].push(fn)
    },
    trigger(key,...args){
        if(!this.clientList[key]){
            return false
        }
        this.clientList[key].forEach(fn=>fn.apply(this,args))
    }
}

再定义一个installEvent函数,用于安装发布订阅功能:

function installEvent(obj){
    for(let key in event){
        obj[key] = event[key]
    }
}

然后我们就可以在任何餐厅使用这个发布订阅功能了:

const salesOffices = {}
installEvent(salesOffices)
salesOffices.listen('单人座',fn1 = function(number){
    console.log('号码',number)
})
salesOffices.listen('四人座',fn2 = function(number){
    console.log('号码',number)
})
salesOffices.trigger('单人座',10)
salesOffices.trigger('四人座',12)

取消订阅

有时候我们可能需要取消订阅,我们可以给listen方法添加一个remove方法,用于取消订阅:

event.remove = function(key,fn){
    if(!this.clientList[key]){
        return false
    }
    this.clientList[key] = this.clientList[key].filter((item) => item !== fn);
}

这样我们就可以取消订阅了:

event.remove('单人座',fn1)
event.remove('四人座',fn2)
salesOffices.trigger('单人座',10)
salesOffices.trigger('四人座',12)

现实案例-模块间通信

现在有两个模块,a 模块里面有一个按钮,每次点击按钮之后,b 模块里的 div 中会显示 按钮的总点击次数,我们用全局发布—订阅模式完成下面的代码,使得 a 模块和 b 模块可以在保 持封装性的前提下进行通信。

<!DOCTYPE html> 
<html> 
<body> 
 <button id="count">点我</button> 
 <div id="show"></div> 
</body> 
<script type="text/JavaScript"> 
var a = (function(){ 
 var count = 0; 
 var button = document.getElementById( 'count' ); 
 button.onclick = function(){ 
    Event.trigger( 'add', count++ ); 
 } 
})(); 
var b = (function(){ 
 var div = document.getElementById( 'show' ); 
    Event.listen( 'add', function( count ){ 
        div.innerHTML = count; 
    }); 
})(); 
</script> 
</html>

总结

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。 发布-订阅模式是一种松耦合的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。 发布-订阅模式是一种常见的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。

优点

  • 松耦合设计
  • 可扩展性
  • 可复用性
  • 可维护性

缺点

  • 性能问题
  • 内存泄漏
  • 耦合性高
  • 命名重合问题

适用场景

  • 异步编程
  • 事件驱动编程
  • 模块间通信 发布-订阅模式是一种常见的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。