定义
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。
实际上,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>
总结
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。 发布-订阅模式是一种松耦合的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。 发布-订阅模式是一种常见的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。
优点
- 松耦合设计
- 可扩展性
- 可复用性
- 可维护性
缺点
- 性能问题
- 内存泄漏
- 耦合性高
- 命名重合问题
适用场景
- 异步编程
- 事件驱动编程
- 模块间通信 发布-订阅模式是一种常见的设计模式,它让两个对象之间没有直接的联系,而是通过 一个中介者对象进行通信。