定义
发布订阅模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
应用
购房者-售楼处,购房者把电话留在售楼处,购房者关注楼盘信息更新,售楼处会依次短信通知。
作用
- 时间上解耦(在异步编程中使用发布—订阅模式,无需过多关注对象在异步运行期间的内部状态,而只需要订阅关注的事件节点)
- 对象间解耦(一个对象不用再显式地调用另外一个对象的某个接口)
DOM 事件
document.body.addEventListener( 'click', function(){ xxx; }, false );
document.body.click(); // 模拟用户点击
订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。
还可以随意增加或者删除订阅者,增加删除任何订阅者都不会影响发布者代码的编写。
document.body.addEventListener( 'click', function(){ xxx2; }, false );
document.body.addEventListener( 'click', function(){ xxx3; }, false );
document.body.removeEventListener( 'click', function(){ xxx3; }, false );
document.body.addEventListener( 'click', function(){ xxx4; }, false );
document.body.click(); // 模拟用户点击
自定义事件
实现发布-订阅模式的步骤:
- 指定好发布者
- 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
- 发布消息的,发布者遍历缓存列表,依次触发里面存放的订阅者回调函数
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 ){
// 小明订阅88平方米房子的消息
console.log( ’价格= ' + price );
// 输出: 2000000
});
salesOffices.listen( 'squareMeter110', function( price ){
// 小红订阅110平方米房子的消息
console.log( ’价格= ' + price );
// 输出: 3000000
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布88平方米房子的价格
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布110平方米房子的价格
通用实现
提取发布-订阅功能:
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时带上的参数
}
}
};
给对象安装发布-订阅功能:
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
上述实现的发布-订阅模式还存在两个问题:
- 每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
- 订阅者跟发布者还是存在一定的耦合性,订阅者至少要知道发布的事件名,才能顺利的订阅到事件。
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 ); // 售楼处发布消息
先发布后订阅
利用一个数组将发布过的消息缓存起来,后续如果有订阅 从数组找到这一条执行,这些缓存的消息执行一次后要从数组删除。
模块间通信
利用Event对象可以在两个模块间通信,这两个模块可以完全不知道对方的存在。
a模块:
function(){
Event.tigger('add',function(){})
}
b模块:
function(){
Event.listen('add',function(){})
}
缺点:
使用Event通信太多,会不清楚消息来源及流向,难以维护。项目中如非必要,尽量少使用Event通信。