设计模式——观察者模式
观察者模式又叫发布/订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主对象。当主对象的状态发生变化时就会通知所有观察者对象进行更新。
事件模型
在JS中我们一般使用事件模型来替代传统的观察者模式。只要我们在DOM节点上绑定过事件函数,那我们就使用过观察者模式了。来看个最简单的例子:
document.body.addEventListener('click', function() {
alert('clicked');
}, false);
我们订阅了document.body上的click事件,当body被点击时触发click事件,就向订阅者发布通知,触发回调函数。
除了DOM事件,我们也可以自定义事件来实现观察者模式。流程如下:
- 指定发布者。
- 给发布者对象添加缓存列表,用于存放回调函数以便触发事件时通知订阅者执行。
- 事件触发时发布者遍历缓存列表,依次触发缓存列表里存放的订阅者回调函数。
一个全局观察者模式的通用实现如下:
const Event = (function(){
let clientList = {},
listen,
trigger,
remove;
listen = function(key, fn) { // 监听
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger = function() { // 触发
let key = Array.prototype.shift.call(arguments),
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (let i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
},
remove = function(key, fn) { // 移除
let fns = this.clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0) // 取消所有订阅
} else {
for (let l = fns.length - 1; l >= 0; l--) {
let _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1); // 删除订阅回调函数
}
}
}
};
return { // 暴露全局接口
listen,
trigger,
remove
}
})();
全局通信实例
基于上一节实现的全局观察者模式,我们就可以在两个模块间进行事件通信了。假设要实现这样一个需求: a模块中有一个button,点击之后触发add事件,b模块监听到该事件后会显示按钮点击次数。我们用全局观察者模式实现如下:
let a = (function() {
let count = 0;
let button = document.getElementById('count');
button.onclick = function() {
Event.trigger('add', count++);
}
})();
let b =(function() {
let div = document.getElementById('show');
Event.listen('add', function(count) {
div.innerHTML = count;
})
})();
至此我们就用观察者模式实现了一个简单的全局事件通信。功能虽然实现了,但仍然存在可以改进的地方。
观察者模式改进
我们目前实现的观察者模式,都是必须先订阅一个消息推入缓存,才能在触发事件后接收消息。但在不少的需求中,我们都需要先把消息接收到,等到有订阅时再发布给订阅者。比如QQ中的离线消息,当用户接收到消息可能还并没有订阅,等到用户上线时才触发订阅事件,接收到发布者的离线消息。为了满足这个需求,我们需要将离线事件缓存,当发布事件触发时,如果没有订阅事件则将发布回调函数继续包装后缓存。等到有对象订阅时再遍历取出执行。
除了订阅和发布的时间先后问题,我们还必须注意到这个观察者模式是基于全局对象的,一不小心就会出现变量名冲突的情况,所以我们还可以通过命名空间来保护这个观察者模式。具体优化的代码如下:
const Event = (function() {
let global = this,
Event,
_default = 'default';
Event = function() {
let _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function(ary, fn) {
let ret;
for (let i = 0, l = ary.length; i < l; i++) {
let 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 (let i = cache[key].length;i>=0;i--) {
if (cache[key][i] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
_trigger = function() {
let 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) {
let namespace = namespace || _default;
let cache = {},
offlineStack = [],
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() {
let 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;
};
return {
create: _create,
one: function(key, fn, last) {
let event = this.create();
event.one(key, fn, last);
},
remove: function(key, fn) {
let event = this.create();
event.remove(key, fn);
},
listen: function(key, fn, last) {
let event = this.create();
event.listen(key, fn, last);
},
trigger: function() {
let event = this.create();
event.trigger.apply(this, arguments);
}
};
}();
return Event;
})();
// 先发布后订阅实例
Event.trigger('click', 1);
Event.listen('click', function(a) {
console.log(a);
});
// 命名空间
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);
小结
观察者模式在实际开发中非常有用,目前常见的MV*框架的数据绑定也离不开观察者模式。它不仅解耦了对象还解耦了时间,可以广泛应用于异步编程中。
-- EOF --