JavaScript设计模式六(发布-订阅模式)
发布-订阅模式又叫做观察者模式,定义:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖于它的对象都将得到通知。在JavaScript中的,我们一般用事件模型来代理传统的发布-订阅模式
发布-订阅模式的例子
例如小明想要买房子,去售楼处去问了,售楼小姐姐告诉他买完了,后续会加推楼盘。小明要了售楼小姐姐的电话,隔一段时间就问楼盘出来了么没有。然后可能小洪、小熊也是这样,然后售楼小姐姐就离职了。。。。
but,实际上售楼处的小姐姐不会这么傻,她们会让小明这一类的客户把电话留下,当楼盘出来了,它再一个个的电话通知
这个例子其实就是典型的发布订阅模式。小明、小洪都是订阅者,她们订阅了房子出售的信息,售楼处作为发不者,会在合适的时候依次给购房者打电话。这么做的好处在于购房者不用天天打电话咨询开售时间,售楼处的变动也不会影响购买者。
发布订阅模式的作用:
- 发布-订阅模式广泛应用于异步编程中,这是替代回调函数的一种方法,例如node中eventproxy
- 发布-订阅模式可以替代对象之间的硬编码的通知机制,一个对象不用再显式的调用另外一个对象的某个接口
JavaScript中的事件模型
JavaScript中有两种事件模型:DOM0级、DOM2级
- DOM0级是早期的事件模型,所有的浏览器都是支持的,而且实现也是比较简单的,代码如下:
// 绑定事件
document.getElementById('test').onclick = function(){
console.log('哈哈哈');
}
// 解除事件
document.getElementById('test').onclick = null;
在DOM0级中,只能绑定一个同类型的函数,如果注册多个,就会发生覆盖
- DOM2级
document.getElementById('test').addEventListener('click', function(){
console.log('哈哈哈');
}, false);
如何实现自定义事件
除了DOM事件之外,还会有一些自定义的事件,接下来我们看如何实现发布-订阅模式
- 首先指定好谁充当发布者
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
- 最后发消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数
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);
}
}
// 然后开始订阅
// 小明
salesOffices.listen(function(price, squreMeter) {
console.log(price, squreMeter);
})
// 小红
salesOffices.listen(function(price, squreMeter) {
console.log(price, squreMeter);
})
salesOffices.trigger( 2000000, 88 );
这里算上线了基本的发布订阅模式,但是还是有点问题,比如小明不想要88平的,结果售楼处把88平的房子信息发给了小明。解决方法就是增加一个标志
var salesOffices = {}
salesOffices.chatList = [];
salesOffices.listen = function(key, fn){
if (!this.chatList[key]) {
this.chatList[key] = [];
}
this.chatList[key].push(fn);
};
salesOffices.trigger = function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.chatList[key];
if (!fns || fns.lenght == 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];){
fn.apply(this, arguments);
}
};
salesOffices.listen('squreMeter88', function(price){
console.log(price);
});
salesOffices.listen('squreMeter110', function(price){
console.log(price);
});
salesOffices.trigger('squreMeter88', 200000);
salesOffices.trigger('squreMeter110', 300000);
发布-订阅模式的通用实现
上面的版本已经还不错了,但是小明觉得应该广撒网,不仅仅针对一个售楼处,还需要买其他楼盘的房子,然后综合一下。
var event = {
clientList: [],
listen: function(key, fn) {
if (!this.chatList[key]) {
this.chatList[key] = [];
}
this.chatList[key].push(fn);
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.chatList[key];
if (!fns || fns.lenght == 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];){
fn.apply(this, arguments);
}
},
};
// 拷贝对象
var installEvent = function(obj) {
for (var i in event) {
obj[i] = event[i];
}
}
这样,我们就可让一个普通对象有发布-订阅的功能
var salesOffices = {};
installEnvent(saleOffices);
salesOffices.listen('squreMeter88', function(price){
console.log(price);
});
salesOffices.listen('squreMeter110', function(price){
console.log(price);
});
salesOffices.trigger('squreMeter88', 200000);
salesOffices.trigger('squreMeter110', 300000);
全局的发布-订阅对象
上面例子还有点问题,就是我们用的拷贝的方式,给每个实例都添加了listen和trigger方法,还有一个缓存列表chatList;此外还有一个问题就是购房者必须要知道楼盘的名字叫salesOffices才能订阅,但是实际生活中并不是这样。购房者有可能是通过中介来买房子,我们可以设计一个全局的Event对象,订阅者和发布者互相是不知道
var Event = (function(){
var clientList = {},
listen, trigger;
listen = function(key, fn) {
if (!chatList[key]) {
chatList[key] = [];
}
chatList[key].push(fn);
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = chatList[key];
if (!fns || fns.lenght == 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];){
fn.apply(this, arguments);
}
},
})();
Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
console.log( '价格= ' + price ); // 输出:'价格=2000000'
});
Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息
必须先订阅再发布么?
在我们做IM通信的过程中,分为消息和消息的回执,如果消息的回执先到客户端,需要先把回执存起来,然后等消息来了之后,把回执拿出来比对一下,更新消息的状态。
所以我们可以新建一个离线栈,当事件发布的时候,如果还没有订阅这个事件,可以先把事件的发布动作包装起来,然后存入栈中,等真正有时间订阅的时候才触发。
全局事件的命名冲突
前面我们用全局事件解决了问题,但是如果各个不同的业务都在全局事件上弄事件,难免会有key的重复,而且业务逻辑又糅合到了一起,这种情况我们可以增加一个全局的事件。
这段代码稍微有点复杂,解决了命名冲突和发布前未订阅的问题。
var Event = (function() {
var global = this,
Event,
_default = 'default';
Event = function() {
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Araay.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function(ary, fn) {
var ret;
for(var i=0, l = fn.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][i] === 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 || _defalut;
var cache = {},
offlineStack = [],
ret = {
listen: function(key, fn, last) {
_listen(key, fn, cache);
if (offlineStack == null) {
return;
}
if (last === 'last') {
offlineStack.lenth && 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, _self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = funciton() {
return _trigger.call(_self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
}
};
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;
})();
使用方法:
// 先发布后订阅
Event.trigger( 'click', 1 );
Event.listen( 'click', function( a ){
console.log( a ); // 输出:1
});
// 使用命名空间
Event.create( 'namespace1' ).listen( 'click', function( a ){
console.log( a ); // 输出:1
});
Event.create( 'namespace1' ).trigger( 'click', 1 );
Event.create( 'namespace2' ).listen( 'click', function( a ){
console.log( a ); // 输出:2
});
Event.create( 'namespace2' ).trigger( 'click', 2 );