发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在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 ); // arguments 是发布消息时带上的参数
}
};
下面我们来进行一些简单的测试:
salesOffices.listen( function( price, squareMeter ) { // 小明订阅者
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );
}); salesOffices.listen( function( price, squareMeter ) { // 小红订阅者
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );
}); salesOffices.trigger( 2000000, 88 ); // 输出: 价格= 2000000,squareMeter= 88 价格= 2000000,squareMeter= 88
salesOffices.trigger( 3000000, 110 ); // 输出: 价格= 3000000,squareMeter= 110 价格= 3000000,squareMeter= 110
这里还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,虽然小明只想买 88 平方米的房子,但是发布者把110平方米的信息也推送给了小明,这对小明来说是不必要的困扰。所以我们有必要增加一个标示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 ), // 取出消息类型
fns = this.clientList[ key ]; // 取出该消息对应的回调函数集合
if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
return false;
}
for( var i = 0, fn; fn = fns[ i++ ];) {
fn.apply( this, arguments ); // arguments 是发布消息时带上的参数
}
}; salesOffices.listen( 'squareMeter88', function( price ) { // 小明订阅者
console.log( '价格= ' + price );
}); salesOffices.listen( 'squareMeter110', function( price ) { // 小红订阅者
console.log( '价格= ' + price );
}); salesOffices.trigger( 'squareMeter88', 2000000); // 输出: 价格= 2000000
salesOffices.trigger( 'squareMeter110', 3000000); // 输出: 价格= 3000000
二、发布-订阅模式的通用实现
这段代码是否必须在另一个售楼处对象上重写一次呢,有没有办法可以让所有对象都拥有发布—订阅功能呢?
首先,我们把发布—订阅的功能提取出来,放在一个单独的对象内:
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 ); // (2) // arguments 是 trigger 时带上的参数
}
}
}
再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
};
var salesOffices = {};
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', function( price ){
console.log( '价格= ' + price ); // 小明订阅消息
});
salesOffices.listen( 'squareMeter100', function( price ){
console.log( '价格= ' + price ); // 小红订阅消息
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000
salesOffices.trigger( 'squareMeter100', 3000000 ); // 输出:3000000
取消订阅的事件:
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 ); // 删除订阅者的回调函数
}
}
}
}; var salesOffices = {};
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
}
installEvent( salesOffices ); 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 ); // 输出:2000000
三、全局的发布-订阅对象
发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消 息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者” 的角色,把订阅者和发布者联系起来
var Event = (function(){
var clientList = {},
listen,
trigger,
remove;
listen = function( key, fn ){
if ( !clientList[ key ] ){
clientList[ key ] = [];
}
clientList[ key ].push( fn );
};
trigger = function(){
var key = Array.prototype.shift.call( arguments ),
fns = clientList[ key ];
if ( !fns || fns.length === 0 ){
return false;
}
for( var i = 0, fn; fn = fns[ i++ ]; ){
fn.apply( this, arguments );
}
}; remove = function( key, fn ){
var fns = clientList[ key ];
if ( !fns ){
return false;
}
if ( !fn ){
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 );
}
}
}
}; return {
listen: listen,
trigger: trigger,
remove: remove
}
})(); Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
console.log( '价格= ' + price ); // 输出:'价格=2000000'
});
Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息
四、总结
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与,而且 JavaScript 本身也是一门基于事件驱动的语言。
当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个 bug 不是件轻松的事情。