发布订阅模式,通过定义一个发布者对象,并建立订阅者对发布者的依赖,实现发布者向订阅者传递数据并执行订阅者指定的回调:
var publisher = {
callbacks:{}, // 订阅记录存放对象
subscribe:function(name,callback){ // 订阅
this.callbacks[name] ? this.callbacks[name].push(callback) : this.callbacks[name] = [callback]
},
publish:function(name,data){ // 发布
if(this.callbacks[name]){
for(i in this.callbacks[name]){ // 遍历执行该订阅者全部回调函数
this.callbacks[name][i](data)
}
}
}
}
上面的代码定义了一个简单的发布者对象,包含订阅者列表对象,订阅方法,发布方法。使用如下:
publisher.subscribe('Ronnie',function(msg){
console.log('this is Ronnie\'s msg :' + msg )
});
publisher.subscribe('Ronnie',function(msg){
console.log('this is Ronnie\'s new msg :' + msg )
});
publisher.subscribe('Robbin',function(msg){
console.log('this is Robbin\'s msg :' + msg )
});
上面的代码新增了两个订阅者,并指定订阅回调函数,其中Ronnie订阅了两个事件,下面的代码将为他们发布数据:
publisher.publish('Ronnie','Hello Ronald')
//this is Ronnie's msg :Hello Ronald
//this is Ronnie's new msg :Hello Ronald
publisher.publish('Robbin','Hello Robert')
//this is Robbin's msg :Hello Robert
上面的代码可以看出两个订阅者的回调分别被执行并拿到了各自的数据。
通过发布订阅模式,可以实现很多场景需求,尤其在触发异步调用函数时。例如:通过全局状态管理各模块更新。下面的代码将逐步构建一个功能比较完整的发布者对象:
var publisher = {
callbacks:{}, // 订阅记录存放对象
subscribe:function(name,callback){ // 订阅
this.callbacks[name] ? this.callbacks[name].push(callback) : this.callbacks[name] = [callback]
},
cancel:function(name){ // 取消订阅
if(this.callbacks[name]){
delete this.callbacks[name]
}
}
publish:function(name,data){ // 发布
if(this.callbacks[name]){
for(i in this.callbacks[name]){ // 遍历执行该订阅者全部回调函数
this.callbacks[name][i](data)
}
}
}
}
上面的代码为发布者添加了取消订阅方法,但是不够完善,继续优化:
var publisher = {
callbacks:{}, // 订阅记录存放对象
subscribe:function(name,callbackName,callback){ // 订阅
if(!this.callbacks[name]){
this.callbacks[name] = {} // 使用字典代替数组来保存订阅者的回调方法
}
this.callbacks[name][callbackName] = callback
},
cancel:function(name,callbackName){ // 取消订阅
if(this.callbacks[name]){
if(callbackName){ // 取消指定订阅者的指定回调方法
delete this.callbacks[name][callbackName]
}else{ // 取消指定订阅者所有回调方法
delete this.callbacks[name]
}
}
}
publish:function(name,data){ // 发布
if(this.callbacks[name]){
for(i in this.callbacks[name]){ // 遍历执行该订阅者全部回调函数
this.callbacks[name][i](data)
}
}
}
}
经过上面的优化后,可以更加精准的控制取消订阅的回调函数,下面将优化发布方法:
var publisher = {
callbacks:{}, // 订阅记录存放对象
subscribe:function(name,callbackName,callback){ // 订阅
if(!this.callbacks[name]){
this.callbacks[name] = {} // 使用字典代替数组来保存订阅者的回调方法
}
this.callbacks[name][callbackName] = callback
},
cancel:function(name,callbackName){ // 取消订阅
if(this.callbacks[name]){
if(callbackName){ // 取消指定订阅者的指定回调方法
delete this.callbacks[name][callbackName]
}else{ // 取消指定订阅者所有回调方法
delete this.callbacks[name]
}
}
}
publish:function(name,callbackName = ''){ // 发布
if(this.callbacks[name]){
let args = arguments.slice(2) //获取去掉name和callbackName后的参数数组
if(callbackName){ // 执行指定订阅者指定回调
this.callbacks[name][callbackName].apply(this,args) // 利用apply方法将数组展开,作为参数列表传给回调方法
}else{
for(i in this.callbacks[name]){ // 执行指定订阅者全部回调
this.callbacks[name][i].apply(this,args)
}
}
}
}
}
现在这个发布者的发布方法可以精确控制订阅这要执行的回调函数,同时可以将任意个数的参数传递给回调函数。
通常情况下,通过先订阅在发布的方式,前面代码中的发布者对象都可以正常执行,如果是先发布后订阅,则无法执行回调,下面的代码将为发布者增加先发布后订阅功能:
var publisher = {
callbacks:{}, // 订阅记录存放对象
preCallbacks:{},// 增加一个存放先发布后订阅时订阅记录的对象
subscribe:function(name,callbackName,callback){ // 订阅
if(!this.callbacks[name]){
this.callbacks[name] = {} // 使用字典代替数组来保存订阅者的回调方法
}
this.callbacks[name][callbackName] = callback
if(this.preCallbacks[name][callbackName] && this.preCallbacks[name][callbackName].length){ //如果在订阅之前有过发布记录
for(i in this.preCallbacks[name][callbackName]){ // 使用订阅之前发布时保存的参数执行发布记录
callback.apply(this,this.preCallbacks[name][callbackName][i])
}
delete this.preCallbacks[name][callbackName]
}
},
cancel:function(name,callbackName){ // 取消订阅
if(this.callbacks[name]){
if(callbackName){ // 取消指定订阅者的指定回调方法
delete this.callbacks[name][callbackName]
}else{ // 取消指定订阅者所有回调方法
delete this.callbacks[name]
}
}
}
publish:function(name,callbackName = ''){ // 发布
let args = arguments.slice(2) //获取去掉name和callbackName后的参数数组
if(this.callbacks[name]){
if(callbackName){ // 执行指定订阅者指定回调
this.callbacks[name][callbackName].apply(this,args) // 利用apply方法将数组展开,作为参数列表传给回调方法
}else{
for(i in this.callbacks[name]){ // 执行指定订阅者全部回调
this.callbacks[name][i].apply(this,args)
}
}
}else{ //如果callbacks中没有对应订阅者则在precallbacks中添加
if(!this.preCallbacks[name]){
this.preCallbacks[name] = {}
}
if(!this.preCallbacks[name][callbackName]){
this.preCallbacks[name][callbackName] = [] // 在precallbacks中记录待发布回调函数
}
this.preCallbacks[name][callbackName].push(args) // 保存待发布回调函数的参数
}
}
}
通过上面的代码,可以实现在发布时如果没有找到对应订阅则保存这一次发布记录,当订阅时检测是之前的发布记录,如果有,则立即执行之前的发布操作。
下面我们还可以继续优化和完善这个发布者对象本身的代码:
var publisher = (function(){
let callbacks = {}
let preCallbacks = {}
function subscribe( name, callbackName, callback ){
if( !name || !callbackName || !callback){
return
}
if ( !callbacks[ name ] ){
callbacks[ name ] = {};
}
callbacks[name][callbackName] = callback;
if(preCallbacks[name] && preCallbacks[name][callbackName] && preCallbacks[name][callbackName].length){
preCallbacks[name][callbackName].forEach( args => {
callback.apply(this,args)
})
delete preCallbacks[name][callbackName]
}
};
function cancel( name, callbackName ){
if( !name ){
return
}
if ( callbacks[ name ] ){
if( callbackName ){
delete callbacks[name][callbackName]
}else{
callbacks[name] = [];
}
}
};
function publish( ){
let subscriber = Array.prototype.shift.call(arguments)//arguments还特么不是数组,类数组对象我擦,取个元素还得call数组原型的方法,妈的
if( !subscriber || !subscriber.name ){
return
}
let args = arguments
if(callbacks[subscriber.name]){
if(subscriber.callback){ // 执行指定订阅者指定回调
callbacks[subscriber.name][subscriber.callback].apply(this,args) // 利用apply方法将数组展开,作为参数列表传给回调方法
}else{
for(let key in callbacks[subscriber.name]){ // 执行指定订阅者全部回调
callbacks[subscriber.name][key].apply(this,args)
}
}
}else{ //如果callbacks中没有对应订阅者则在precallbacks中添加
if(!preCallbacks[subscriber.name]){
preCallbacks[subscriber.name] = {}
}
if(!preCallbacks[subscriber.name][subscriber.callback]){
preCallbacks[subscriber.name][subscriber.callback] = [] // 在precallbacks中记录待发布回调函数
}
preCallbacks[subscriber.name][subscriber.callback].push(args) // 保存待发布回调函数的参数
}
};
return {subscribe, cancel, publish}
})()
通过上面的代码已经能够实现一个相对完整的发布者对象,试用一下:
publisher.subscribe('Ronnie','msg1',function(msg){
console.log('this is Ronnie\'s msg1 :' + msg )
});
publisher.subscribe('Ronnie','msg2',function(msg){
console.log('this is Ronnie\'s msg2 :' + msg )
});
publisher.subscribe('Robbin','msg1',function(msg){
console.log('this is Robbin\'s msg1 :' + msg )
});
注册了Ronnie和Robbin两个订阅者,其中Ronnie具有msg1和msg2两个回调函数,Robbin只有msg1回调,下面尝试对他们发布数据:
publisher.publish({name:'Ronnie'},'Hello Ronald')
//this is Ronnie's msg1 :Hello Ronald
//this is Ronnie's msg2 :Hello Ronald
publisher.publish({name:'Robbin',callback:'msg1'},'Hello Robert')
// this is Robbin's msg1 :Hello Robert
publisher.publish({name:'Robbin',callback:'msg2'},'Hello Robert')
// 没有输出
可以看到Ronnie的回调全部执行了,Robbin的msg2方法由于没有订阅而不输出任何信息,下面为Robbin订阅msg2回调方法:
publisher.subscribe('Robbin','msg2',function(msg){
console.log('this is Robbin\'s msg2 :' + msg )
});
// this is Robbin's msg2 :Hello Robert
可以看到订阅之前的发布被执行了。