发布-订阅模式又叫观察者模式,它定义与对象间的一种一对多的关系,当一个对象的状态发生改变时,所有依赖与它的对象都将得到通知。
发布订阅的作用
-
发布-订阅模式可以广泛应用与异步编程中,这是一种替代传统回调函数的方案。
- 比如订阅ajax的succ,error等事件,只需订阅事件发生点,而无需关注异步运行期间状态
-
发布-订阅可以取代对象之间硬编码的通信机制,一个对象不再显式地调用另外一个对象的某个接口。
- 发布-订阅模式让两个对象松散耦合地联系在一起,虽然不清楚彼此的细节,但不影响它们之间的通信。
- 当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者改变时,也不会影响到之前的订阅者。
发布-订阅模式的通用实现
var event = {
//缓存列表,存放订阅者的回调函数
clientList: [],
//增加订阅者, key:事件名,fn:回调函数
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)
// 取出消息类型对应的回调函数集合
fns = this.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 = this.clientList[key]
// 如果对应的消息没人订阅,直接返回
if(!fns) {
return false
}
// 如果没有传入回调函数,表示取消key对应消息的所有订阅
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)
}
}
}
}
}
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('价格1:'+price)
})
salesOffices.listen('squareMeter87', fn2 = function(price){
console.log('价格2:'+price)
})
salesOffices.trigger('squareMeter88', 20000) // 价格1: 20000
salesOffices.remove('squareMeter88')
salesOffices.trigger('squareMeter88', fn1) // 打印结果为空
其实在这个发布-订阅模式中还存在两个问题:
- 给对象单独添加其实是一种资源浪费
- 发布者与订阅者存在一定耦合性,至少要知道
salesOffices
这个对象
所以接下来实现一个全局的发布-订阅对象
var Event = (function(){
var clientList = {},
listten,
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,
trigger,
remove
}
})()
Event.listen('squareMeter88', fn1 = function(price){ // 订阅消息
console.log('价格1:'+price)
})
Event.trigger('squareMeter88', 20000) // 价格1: 20000
必须先订阅在发布吗
我们所了解的订阅发布都是订阅者必须先订阅一个消息,随后才能接受到发布者发布的消息。如果顺序反过来,发布者先发布一条消息,而此时没有订阅者,无疑消息会消失。
探讨下这种需求
在某些情况下,我们需要先讲这条消息保存下来,等到有对象订阅它的时候,再重新把消息发布给订阅者,如果QQ中的离线消息,离线消息先被保存在服务器中,接收人下次登陆之后,可以从新接收到这条消息。
再假如一个商城系统中,获取到用户信息后才能渲染导航模块,无疑这是一个异步的过程,不能保证ajax何时返回。所以我们需要我们发布订阅对象拥有先发布后订阅的能力。
如何实现呢
为了满足这种需要,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包裹函数被存入堆栈中,等到终于有对象来订阅此事件,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ未读消息只会被重新阅读一次。
命名冲突
全局的订阅-发布对象里只有一个clientList来存放消息名和回调函数,我们都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以需要给Event对象提供创建命名空间的功能。
具体实现
var Event = (function() {
var Event,
_default = 'default';
Event = function() {
var _listen,
_trigger,
_remove,
_shift = [].shift,
_unshift = [].unshift,
namespaceCache = {},
_create,
each = function(arr, fn) {
var ret;
for (var i = 0, l = arr.length; i < l; i++) {
var n = arr[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 - 1; i >= 0; i--) {
if (cache[key][i] === fn) {
// 删除订阅回调函数
cache[key].splice(i, 1);
}
}
}
else {
cache[key] = [];
}
}
};
_trigger = function() {
var cache = _shift.call(arguments);
var key = _shift.call(arguments);
var args = arguments;
var _this = this;
var stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function() {
return this.apply(_this, args);
})
};
_create = function(namespace) {
namespace = namespace || _default;
var cache = {};
var offlineStack = []; //离线事件
var ret = {
listen: function(key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
if (last === 'last') {
offlineStack.length && offlineStack.pop()();
}
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,
_this = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function() {
return _trigger.apply(_this, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
};
return namespace ?
(namespaceCache[namespace] ?
namespaceCache[namespace] : namespaceCache[namespace] = ret)
: 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;
}());
var fn1 = function(price) {
console.log(price);
};
// 实例
Event.listen('squareMeter88', fn1);
Event.remove('squareMeter88', fn1);
Event.listen('squareMeter88', function(price) {
console.log('fn2: ' + price);
});
Event.trigger('squareMeter88', 20000); // fn2: 20000
总结
优点
时间与对象之间的解耦,应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写,从架构上看,无论MVC还是MVVM都少不了发布-订阅的参与
缺点
创建订阅者本身要消耗一定时间和内存,无论消息是否发生,一直存在内存中。虽然发布-订阅模式虽然可以弱化对象之间的联系,但是过度使用,对象之间的必然联系会被深埋,特别是多个订阅者和发布者嵌套会造成难以追踪的bug。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。