观察者模式,也叫订阅-发布模式。顾名思义,就是订阅某些功能,然后在适当的时机发布出来,也就是执行这些功能。
订阅:就是把几个函数推入数组中待用;
发布:就是把缓存在数组中的函数拿出来执行;
var login = {};
login.eventList = {};
//将函数推入数组中保存,待用
login.listen = function(key, fn) {
if(!this.eventList[key]) {
this.eventList[key] = [];
}
this.eventList[key].push(fn);
}
login.trigger = function(key) {
var fns = this.eventList[key];
if(!fns || fns.length === 0) {
return false;
}
for(var i=0; i<fns.length;i++) {
fns[i]();
}
}
//订阅
login.listen('loginSuccess', function() {
console.log('显示用户头像');
})
login.listen('loginSuccess', function() {
console.log('显示消息列表');
})
//发布
login.trigger('loginSuccess');
应用场景:
现在前端领域,SPA单页应用已经非常普遍了,每个页面,都是用ajax异步请求。ajax请求有一个比较闹心的问题,就是层级回调。比如:
有一个页面,需要调用三个数据接口。
第一个是login登录接口,
第二个是根据登录接口返回的id,调取头像接口。
第三个是根据登录接口返回的id,调取消息列表接口。
一般情况下会这么写:
$.ajax({
url: 'http://ajax.login.com',
dataType: 'json',
success: function(data) {
getAvatar(data.id);
getMsg(data.id);
...
}
})
这样写虽然没有问题,但却不容易维护。如果哪天改了需求,需要加个接口,你还得翻出这段代码,找到success回调,再加上一个函数。加函数还算好的,有的人会直接在success回调里继续写$.ajax这样的代码,一级一级的这么摞着写,这样代码很快就会变成一堆大便,变得不可维护。这种写法叫做造粪模式,百分百的造出垃圾来。因为耦合性太大,接口调用都成了拴在一条绳子上的蚂蚱,一扯就是一坨。
如何解耦呢?就是利用订阅发布模式,我们可以在getAvatar方法中,订阅(listen)login接口,而一旦login接口走到success回调,我们就发布(trigger)一下
var event = {
eventList: {},
listen: function(key, fn) {
if(!this.eventList[key]) {
this.eventList[key] = [];
}
this.eventList[key].push(fn);
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.eventList[key];
if(!fns || fns.length === 0) {
return false;
}
for(var i=0; i<fns.length; i++) {
fns[i].apply(this, arguments);
}
}
};
var installEvent = function(obj) {
//浅拷贝
for(var i in event) {
obj[i] = event[i];
}
}
var login = {};
installEvent(login);
//订阅
login.listen('loginSuccess', function() {
console.log('显示用户头像');
});
login.listen('loginSuccess', function() {
console.log('显示消息列表');
});
//发布
login.trigger('loginSuccess');
现在订阅没有问题了,那如何取消订阅呢?我们再加上取消订阅函数
var event = {
eventList: {},
listen: function(key, fn) {
if(!this.eventList[key]) {
this.eventList[key] = [];
}
this.eventList[key].push(fn);
},
remove: function(key, fn) {
var fns = this.eventList[key];
if(!fns) {
return false;
}
if(!fn) {
//如果没有回调,表示取消此key下的所有方法
fns && (fns.length);
} else {
for(var i=0; i<fns.length; i++) {
if(fns[i] == fn) {
fns.splice(i, 1);
}
}
}
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.eventList[key];
if(!fns || fns.length === 0) {
reutrn false;
}
for(var i=0; i<fns.length; i++) {
fns[i].apply(this, arguments);
}
}
};
var installEvent = function(obj) {
for(var i in event) {
obj[i] = event[i];
}
};
var login = {};
installEvent(login);
//显示用户头像
function showAvatar() {
console.log('显示头像数据');
};
//显示消息列表
function showMessage() {
console.log('显示消息列表');
};
//订阅
login.listen('loginSuccess', showAvatar);
login.listen('loginSuccess', showMessage);
//发布
login.trigger('loginSuccess');
//取消订阅
login.remove('loginSuccess', showAvatar);
//再次发布
login.trigger('loginSuccess');
我们的订阅发布模式走到这里,基本上已经完善了。最后我们来看一下ajax回调问题怎么来解决。我们其实根本不需要在登录的ajax回调中加拉取头像等逻辑,而只需让拉取头像功能订阅登录接口即可,当登录工作完成后会发布,也就是触发缓存在数组中的函数执行。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>订阅-观察者模式</title>
<script src="http://mockjs.com/dist/mock.js"></script>
<script src="http://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
</head>
<body>
</body>
<script>
Mock.mock('http://ajax.login.com', {
'name': '@name',
'age|1-100': 1
});
var event = {
eventList: {},
listen: function(key, fn) {
if(!this.eventList[key]) {
this.eventList[key] = [];
}
this.eventList[key].push(fn);
},
remove: function(key, fn) {
var fns = this.eventList[key];
if(!fns) {
return false;
}
if(!fn) {
fns && fns.length = 0;
} else {
for(var i=0; i<fns.length; i++) {
if(fn == fns[i]) {
fns.splice(i, 1);
}
}
}
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.eventList[key];
if(!fns || fns.length === 0) {
return false;
}
for(var i=0; i<fns.length; i++) {
fns[i].apply(this, arguments);
}
}
};
var installEvent = function(obj) {
for(var i in event) {
obj[i] = event[i];
}
};
var login = {};
installEvent(obj);
var avatar = (function() {
login.listen('loginSucc', function() {
avatar.setAvatar(data);
});
return {
setAvatar: function(data) {
console.log('显示用户' + data['name'] + '的头像');
}
}
})();
var message = (function() {
login.listen('loginSucc', function(data) {
message.setMsg(data);
});
return {
setMsg: function(data) {
setTimeout(function() {
console.log('显示用户' + data['name'] + '的消息');
})
}
}
})();
//发布
$.ajax({
url: 'http://ajax.login.com',
dataType: 'json',
success: function(data) {
login.trigger('loginSucc', data);
}
})
</script>
<html>
事实上,还有一种更普遍意义的订阅发布模式。比如在一个按钮上绑定click事件,这其实就是一个订阅的过程;而鼠标点击就是发布。