介绍
发布-订阅模式又称观察者模式。它定义对象间的一对多的依赖关系。当一个对象状态发生改变时,所有依赖于他的对象都将得到通知。
白话介绍
小明看上一套房子,被告知已经卖完了,不过下一期即将开盘,具体时间、价格不知,于是乎售楼处让小明留下手机号。后面还有小白 小兰 小红分别登记了手机号。在新楼盘推出的时候,售楼销售会翻开花名册,遍历上面的电话号码,依次发送通知短信给他们
全局 发布-订阅对象例子
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 ); // 售楼处发布消息
必须先订阅后发布吗
我们了解的发布-订阅模式都是订阅者先订阅一个消息,随后才能接收到发布者发布的消息。例如我们现在要实现一个类似qq离线消息列表一样的功能。在离线时(未订阅)收到的消息存起来,等到我们登录后(订阅时),再次推送给我们消息,当然这些消息是一次性的。
全局的发布-订阅模式只有一个clientList来存放消息和回调,当我们项目规模打起来后,避免不了出现命名冲突的情况,所以我们还可以给Event提供一个创建命名空间的功能。
我们先来看一看如何使用:
// 先发布后订阅
Event.trigger("click", 1);
Event.listen("click", function (a) {
console.log(a);
});
// 使用命名空间
Event.create("namespace1").listen("click", function (a) {
console.log(a);
});
Event.create("namespace1").trigger("click", 1);
Event.create("namespace2").listen("click", function (a) {
console.log(a);
});
Event.create("namespace2").trigger("click", 2);
具体代码:
var Event = (function () {
var global = this,
Event,
_default = "default";
Event = (function () {
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
/**
* @description 自定义迭代器
* @param {Array} ary 数组
* @param {Object} fn 要实现的方法
* @return {Object} fn
*/
each = function (ary, fn) {
var ret;
for (var i = 0, l = ary.length; i < l; i++) {
var n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
};
// 将订阅者存起来
_listen = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
// 移除订阅者
_remove = function (key, cache, fn) {
if (cache[key]) {
if (fn) {
for (var i = cache[key].length; i >= 0; i--) {
if (cache[key] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
// 发布消息
_trigger = function () {
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
ret,
stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args);
});
};
// 创建命名空间
_create = function (namespace) {
var namespace = namespace || _default;
var cache = {},
offlineStack = [], // 离线事件
ret = {
listen: function (key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
if (last === "last") {
} else {
each(offlineStack, function () {
this();
});
}
offlineStack = null;
},
one: function (key, fn, last) {
_remove(key, cache);
this.listen(key, fn, last);
},
remove: function (key, fn) {
_remove(key, cache, fn);
},
trigger: function () {
var fn,
args,
_self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function () {
return _trigger.apply(_self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
},
};
// return namespace
// ? namespaceCache[namespace]
// ? namespaceCache[namespace]
// : (namespaceCache[namespace] = ret)
// : ret;
if (namespace) {
if (namespaceCache[namespace]) {
return namespaceCache[namespace];
} else {
return (namespaceCache[namespace] = ret);
}
} else {
return ret;
}
};
return {
create: _create,
one: function (key, fn, last) {
var event = this.create();
event.one(key, fn, last);
},
remove: function (key, fn) {
var event = this.create();
event.remove(key, fn);
},
listen: function (key, fn, last) {
var event = this.create();
event.listen(key, fn, last);
},
trigger: function () {
var event = this.create();
event.trigger.apply(this, arguments);
},
};
})();
return Event;
})();
小结
优点:
- 时间上的解耦
- 对象之间的解耦
- 应用广泛,即可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写
- 从架构上看,无论是MVC还是MVVC,都少不了发布-订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言
缺点:
- 创建订阅者本身要消耗一定的时间和内存
- 或许在订阅一个消息后,以后此消息都未发生, 但这个订阅者会始终存在于内存中
- 如果过度使用发布-订阅模式,会导致程序难以跟踪维护,特别是多个发布者和订阅者嵌套在一起的时候,debug是件困难的事情
参考
JavaScript设计模式与开发实践