观察者模式-网站登录
观察者模式也叫发布-订阅者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。最熟悉的使用观察者模式的地方莫过于是在DOM节点上绑定事件函数。
添加事件并不会影响到发布者代码的编写
document.body.addEventListener('click', function() {
alert(1);
}, false);
document.body.addEventListener('click', function() {
alert(2);
}, false);
document.body.addEventListener('click', function() {
alert(3);
}, false);
document.body.click(); //1, 2, 3
定义一个发布-订阅功能的对象:
let event = {
clientList: {},
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) {//如果key对应的消息没有被人订阅,则直接返回
return false;
}
if(!fns) {//如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
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);//删除订阅者的回调函数
}
}
}
}
}
模拟场景1:订阅某一户型的房子的价格信息
let salesOffices = {};
let installEvent = function(obj) {
for (let i in event) {
obj[i] = event[i];
}
}
installEvent(salesOffices);
//小明说我想要88平米的房子,有房源后通知我
salesOffices.listen('squareMeter88', fn1 = function(price){
console.log('价格(小明)=' + price);
})
//小红说我也想要88平米的房子,有房源后通知我
salesOffices.listen('squareMeter88', fn2 = function(price) {
console.log('价格(小红)=' + price);
})
//有88平米的房源了,售价两百万,通知各个订阅的用户
salesOffices.trigger('squareMeter88', 2000000);
//价格(小明)=2000000
//价格(小红)=2000000
//小明说他不需要订阅88平米的房源了
salesOffices.remove('squareMeter88', fn1);
salesOffices.trigger('squareMeter88', 1000000);
//价格(小红)=2000000
模拟场景2:页面上需要显示用户信息,但是我们不知道除了页面header头部、nav导航、消息列表、购物车之外,将来还有哪些模块需要使用这些用户信息。如果它们和用户信息模块产生了强耦合,比如下面这样的形式:
login.succ为登录成功回调
login.succ(function(data) {
header.setAtar(data.avtar);//设置header模块的头像
nav.setAtar(data.avatar);//设置导航模块的头像
messge.refrest();//刷新消息列表
cart.refresh();//刷新购物车列表
...
})
后来需要登录成功后刷新地址列表,就需要在这里添加代码。我们会越来越疲于应付这些突如其来的业务要求。用发布=订阅者模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功后的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务模块接收到消息后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解他们的内部细节。
$ajax('http//xxx.com?login', function(data) {
login.trigger('loginSucc', data);
});
各模块监听登录成功的消息:
let header = (function() {
login.listen('loginSucc', function(data) {
header.setAvatar(data.avtar);
});
return {
setAvatar: function(data) {
console.log('设置header模块的头像');
}
}
})();
全局模式:上面简略实现了一下观察者模式,除此之外还可以创建一个全局对象,降低订阅者对发布者的耦合性,即订阅者不需要知道消息来自哪个发布者,发布者也不知道消息会推送给哪个发布者。即对发布-订阅者模式的封装。
缓存消息:除了可以对发布-订阅者再次封装,还可以设置为不用先订阅再发布,即将发布的内容缓存起来,后面的订阅者也可以接收到此前的消息。此时需要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。