观察者模式
- 又叫发布订阅模式,定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知;
- JS开发中,一般用事件模型替代传统的发布-订阅模式
作用
- 可以广泛应用于异步编程中,替代传统回调函数;如订阅
ajax
请求的error、succ
事件;
- 取代对象之间硬编码的通知机制,一个对象不再显示地调用另一个对象的接口;降低耦合度;
通用实现
var event = {
cacheList: [],
listen: function (key, fn) {
if(!this.cacheList[key])
this.cacheList[key] = [];
this.cacheList[key].push(fn);
},
trigger: function() {
var key = [].shift.call(arguments),
fns = this.cacheList[key];
if(!fns || !fns.length)
return false;
for(var i = 0, fn; fn = fns[i++];)
fn.apply(this, arguments);
},
remove: function(key, fn) {
var fns = this.cacheList[key];
if(!fns)
return false;
if(!fn) { //如果没有传入具体的回调函数则取消所有的
fns && (fns.length = 0);
} else {
for(var len = fns.length - 1; len >= 0; len--) { //反向遍历
var _fn = fns[len];
if(_fn === fn)
fns.splice(len, 1);
}
}
}
};
//动态安装发布-订阅功能;
var installEvent = function(obj) {
for(prop in event) {
if(event.hasOwnProperty(prop))
obj[prop] = event[prop];
}
};
//执行
var publisher = {};
installEvent(publisher);
publisher.listen('click', function(data) {
console.log('clicked! ' + data);
});
publisher.trigger('click', 'new data');
publisher.remove('click');
实际开发中的例子
- 网站开发中多个模块的渲染必须在某个事件(模块)完成之后进行,仅利用回调可能会变成:
login.succ(function(data) {
A.start(data);
B.start(data);
.....
});
- 这个函数耦合度高,承担来太多功能,这时使用观察者模式:
login.succ(function(data) {
loginEvent.trigger('loginSucc', data);
});
var A = (function() {
loginEvent.listen('loginSucc', function(data){
A.start(data);
});
return {
start: function(data) {
consoole.log(data);
}
}
})
全局的发布-订阅对象
- 可以使用一个全局
Event
对象,订阅者不用关心消息来自哪个发布者,发布者不管推送到哪里;
- 去掉
installEvent
优化
var Event = (function() {
var cacheList = {},
listen = function(){},
triggee = function(){},
remove = function(){};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})
可能的缺点
- 消耗一定时间和内存,且当订阅一个消息但都未推送,这个订阅者始终存在于内存;
- 使用太多全局发布-订阅对象,模块与模块之间的联系会被隐藏;会导致程序难以跟踪维护和理解;
命令模式
应用场景
- 向某些对象发送请求,但并不知道请求的接受者是谁,也不知道被请求的操作是什么;
- 将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端实现参数化;
- 传统命令模式实现
/**
* 按钮点击
*/
var btn = document.getElementById('btn');
var setCommand = function (btn, command) {
btn.onclick = function() {
command.execute();
}
};
//实际设置的对象接受对象和请求操作
var Menu = {
num: 0,
add: function() {
this.num++;
console.log(this.num);
}
};
var MenuCommand = function(receive) {
this.receive = receive;
};
MenuCommand.prototype.execute = function() {
this.receive.refresh();
};
//使用
var addMenu = new MenuCommand(Menu);
setCommand(btn, addMenu)
JS中的命令模式
- JS中,由于函数是一等公民;与策略模式一样,已经融入了JS语言之中;
- 其实是回调函数一个面向对象的替代品;
//上面例子的简化
var MenuCommand = function(receive) {
return {
execute: function() {
receive.add();
}
}
};
撤销和重做
var btn = document.getElementById('btn');
var btn2 = document.getElementById('btn2');
var btn3 = document.getElementById('btn3');
var setCommand = function (btn, command, fn) {
btn.onclick = function() {
Menu.commandStack.push(fn);
command[fn]();
}
};
var Menu = {
num: 0,
cache: [],
commandStack: [],
restartNum: 0,
restartCache: [],
add: function() {
this.cache.push(this.num);
this.num++;
console.log(this.num);
},
back: function() {
var cache = this.cache;
if(cache.length)
this.num = cache.pop();
console.log(this.num);
},
restart: function() {
var command;
this.num = this.restartNum;
this.cache = this.restartCache;
this.commandStack.pop();
while (command = this.commandStack.shift())
MenuCommand(this)[command]();
this.restartNum = this.num;
this.restartCache = this.cache;
}
};
var MenuCommand = function(receive) {
return {
execute: function() {
receive.add();
},
undo: function() {
receive.back();
},
restart: function() {
receive.restart();
}
}
};
//使用
var refreshMenu = MenuCommand(Menu);
setCommand(btn, refreshMenu, 'execute');
setCommand(btn2, refreshMenu, 'undo');
setCommand(btn3, refreshMenu, 'restart');
宏命令
- 一组命令的集合,通过执行宏命令可以一次性操作一组命令
var MacroCommand = function () {
return {
commandList: [],
add: function(command) {
this.commandList.push(command);
},
execute: function() {
for(var i = 0, command; command = this.commandList[i++];)
command.execute();
}
}
};
一个标题图片视图的创建
var viewCommand = (function () {
var tpl = {
product: [
'<div>',
'<img src="{#src#}"/>',
'<p>{#text#}</p>',
'</div>'
].join(''),
title: [
'<div class="title">',
'<div class="main">',
'<h2>{#title#}</h2>',
'<p>{#tips#}</p>',
'</div>'
].join('')
};
var html = '';
function formateString (str, obj) {
return str.replace(/{#(w+)#}/g, function (match, key) {
return obj[key];
})
}
var Action = {
create: function (data, view) {
if(data.length) {
for(var i = 0, l = data.length; i < l; i++) {
html += formateString(tpl[view], data[i]);
}
} else {
html += formateString(tpl[view], data);
}
},
display: function (container, data, view) {
if(data) {
this.create(data, view);
}
document.getElementById(container).innerHTML = html;
html = '';
}
};
return function excute(msg) {
msg.param = Object.prototype.toString.call(msg.param) === '[object Array]' ? msg.param : [msg.param];
Action[msg.command].apply(Action, msg.param);
}
})();
var titleData = {
title: 'this is title',
tips: 'this is tips'
};
var productData = [
{
src: '..',
text: 'pig2'
},{
src: '..',
text: 'pig3'
}
];
//创建标题模块
viewCommand({
command: 'display',
param: ['title', titleData, 'title']
});
//创建一个图片
viewCommand({
command: 'create',
param: [{
src: '..',
text: 'pig1'
}, 'product']
})
//创建多个图片
viewCommand({
command: 'display',
param: ['product', productData, 'product']
});