观察者模式又成为订阅发布模式。
目的:解耦各参与者,让各参与者只关注自己应该实现的操作,各参与者通过消息来相互协作。主要用于实现事件机制。
实现:订阅者把自己的某个函数注册到发布者中,该函数通常叫做监听函数(Listener)。当发布者运行到某个事件的时候,回调注册的监听函数。订阅者即收到了发布者发送的通知。
职能: 1)发布者:提供订阅接口,让订阅者可以订阅事件;发布事件。2) 订阅者:对感兴趣的发布者,注册监听函数。
定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
现实中最常见的最简单的情况
//订阅
document.body.addEventListener('click', function() {
console.log('click!');
}, false);
//发布
document.body.click();
自定义事件
1)指定好发布者;2)发布者有一个缓存列表,里面存放了回调函数,以便发布后通知订阅者;
3)发布消息的时候遍历缓存列表,依次触发订阅者的回调;
通用实现
功能:为全局对象 Event 作为一个类似 ‘中介者’,把订阅者跟发布者联系起来。
优点:一是时间上的解耦(异步),二是空间上的解耦;
注意:Event 作为全局对象,处理页面的较大事务时,建议创建命
使用一个全局的Event对象(唯一一个):
var Event = (function() {
var clientList = {},
listen,
trigger,
remove;
remove = function(key, fn) {
var fns = this.clientList[key];
if (!fns) return false;
if (!fn) return fns.length = 0;
for (var i = fns.length - 1; i >= 0; i--) {
if (fn == fns[i]) {
fns.splice(i, 1);
}
}
};
trigger = function() {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || !fns.length) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
listen = function(key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
return {
listen: listen,
trigger: trigger,
remove: remove
};
})();注:我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
必须知道发布者的名字叫salesOffices,一旦想订阅另外一个发布者,我们得再粘一次代码。发布—订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似中介者的角色,把订阅者和发布者联系起来
案例
html 代码
<button id="my">点我</button>
<div id="showCount"></div>
js代码
var count = 0;
var oMy = document.getElementById("my");
oMy.addEventListener("click",function() {
//发布
Event.trigger('add',count++ )
})
var showCount = document.getElementById("showCount")
Event.listen('add',function() {
//订阅
showCount.innerHTML = count
})建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重 新阅读一次,所以刚才的操作我们只能进行一次。
封装
function installEvent (obj) {
for(var i in Event) {
obj[i] = Event[i]
}
console.log(obj)
}案例
var login = {}
function installEvent (obj) {
for(var i in Event) {
obj[i] = Event[i]
}
console.log(obj)
}
installEvent(login)
//订阅
login.listen('loginSucc', function(data) {
if(data.isLogin == "1") {
console.log("已经登录")
}
});
//发布
login.trigger("loginSucc",{
isLogin: "1"
});总结
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。
发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。
当然,发布—订阅模式也不是完全没有缺点(浪费内存)。创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者(b订阅a的消息并发布给c)嵌套到一起的时候,要跟踪一个bug不是件轻松的事情。
知识点
Array.prototype.shift.call(arguments)
arguments是一个类数组对象,虽然有下标,但不是真正的数组,没有shift方法,这时可以通过call或者apply方法调用Array.prototype中的shift方法。
shift 方法
移除数组中的第一个元素并返回该元素