前言:要提升代码水平,就绕不开设计模式。之前也有过一些了解,但并没有深入学习。最近准备系统的学习一下设计模式,提高设计,解耦的能力,发现了一本好书《JavaScript设计模式与开发实践》,所以边读边写,把常用的设计模式学习并记录在这里。
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般使用事件模型来替代传统的发布-订阅模式。
实际上,常用的DOM事件就是发布-订阅模式:
document.body.addEventListener('click', function() {
alert(2);
}, false)
document.body.click(); // 模拟用户点击
我们监听用户点击document.body
的动作,但我们没办法预知用户什么时候点击,所以我们订阅document.body
上的click
事件。当点击发生时,便会向订阅者发布这个消息。
假设这样一个场景: 很多客人在售楼部咨询买房,但目前还没有开盘,所以客人们把自己的联系方式留在售楼部,等到楼房开盘时,售楼部就会根据留下的联系方式,来通知咨询过的客户。
这就是一个典型的发布-订阅模式的场景。
// 全局发布-订阅对象
let Event = (function() {
// 订阅者Map, key为订阅内容关键字,value为通知方法的数组。
let clientList = {},
listen,
trigger,
remove;
// 增加订阅者,传入一个订阅关键字key 和一个通知方法fn
listen = function(key, fn) {
if(!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
// 发布消息,第一个参数为要发布的订阅关键字key,后边的参数为发布时的传参
trigger = function() {
// 取出第一个参数key
let key = Array.prototype.shift.call(arguments),
// 获取通知方法数组
fns = clientList[key];
if(!fns || fns.length === 0) {
return false
}
// 轮流通知
for(let i=0; i<fns.length; i++) {
// 剩下的参数通知时传入
fn.apply(this, arguments);
}
};
// 取消订阅,传入订阅关键字key和要取消的方法
remove = function(key, fn) {
// 获取key对应的方法列表,校验
let fns = clientList[key];
if(!fns){
return false;
}
// 若不传入要取消的方法,则为取消该key下的所有方法
if(!fn) {
fns && (fns.length = 0);
} else {
// 传入时,倒序遍历找到该方法,从数组中删除
for(let i=fns.length-1; i>=0; i--) {
if(fns[i] === fn) {
fns.splice(i, 1);
}
}
}
}
return {
listen,
trigger,
remoce,
}
})();
// 订阅88平米户型
Event.listen('squareMeter88', function() {
console.log(`price: ${price}`);
})
// 售楼处发布消息
Event.trigger('squareMeter88', 20000);
这样的好处在于:
上边的例子中,都是先订阅,发布的时候才会通知。但如果是反过来,还没人订阅的时候就发布了一条消息,那这条消息就消失了。
我们希望能像QQ微信离线消息一样,订阅时可以收到之前发布的消息。为了满足这点,就要建立一个存放离线事件的堆栈,发布时如果还没有订阅者,就先把他储存下来。等终于有对象订阅此事件时,我们再遍历堆栈,重新发布一次,发布之后再从堆栈中删除它。
代码:
// 支持先发布后订阅的 全局发布-订阅对象
let Event = (function() {
// 订阅者Map, key为订阅内容关键字,value为通知方法的数组。
let clientList = {},
waitPublish = [], // 储存没有订阅者的发布事件
listen,
trigger,
remove;
// 增加订阅者,传入一个订阅关键字key 和一个通知方法fn
listen = function(key, fn) {
if(!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
// 当该key是第一次被订阅时,遍历待发布事件,若有则重新发布
if(clientList[key].length === 1) {
for(let i=0; i<waitPublish.length; i++) {
// 重新发布
fn.apply(this);
// 发布后删除
waitPublish.splice(i, 1);
}
}
};
// 发布消息,第一个参数为要发布的订阅关键字key,后边的参数为发布时的传参
trigger = function() {
// 取出第一个参数key
let key = Array.prototype.shift.call(arguments),
// 获取通知方法数组
fns = clientList[key];
// 没有订阅者时
if(!fns || fns.length === 0) {
// 把该发布事件暂存起来
let waitFn = function() {
// 带上参数
trigger(key, ...arguments);
}
waitPublish.push(waitFn);
return false
}
// 轮流通知
for(let i=0; i<fns.length; i++) {
// 剩下的参数通知时传入
fn.apply(this, arguments);
}
};
// 取消订阅,传入订阅关键字key和要取消的方法
remove = function(key, fn) {
// 获取key对应的方法列表,校验
let fns = clientList[key];
if(!fns){
return false;
}
// 若不传入要取消的方法,则为取消该key下的所有方法
if(!fn) {
fns && (fns.length = 0);
} else {
// 传入时,倒序遍历找到该方法,从数组中删除
for(let i=fns.length-1; i>=0; i--) {
if(fns[i] === fn) {
fns.splice(i, 1);
}
}
}
}
return {
listen,
trigger,
remoce,
}
})();
推模型: 在事件发生时,发布者一次性把所有的数据和状态变化推送给订阅者。
拉模型:发布者仅仅通知订阅者事件已经发生,订阅者根据一些公开的借口主动拉取自己需要的数据。
拉模型的好处是可以按需获取,但同时有可能让发布者变的门户大开,也增加了代码的复杂度。JavaScript中的arguments可以方便的表示参数列表,所以我们一般都选择推模型,使用apply方法把所有参数都推送给订阅者。
JavaScript中的发布订阅模式跟其他语言有所不同,Java中通常会把订阅者本身传入发布者中,而JavaScript中,我们注册回调函数的形式来代替,更优雅和简单。
发布-订阅模式的优点:
缺点: