不同的语言,相同的模式
之前有个项目,需要做一个控件的订阅,大致是web将需要订阅的事件值下发给控件,然后当控件触发了订阅的事件时,通知web进行操作。
这个操作在一开始,被我认为是事件触发,后来看到发布-订阅模式,稍稍了解了这大概是它的专业术语。
在控件上有这样的模式,那么js是否也有呢?答案是肯定的,比如trigger
和on
,比如vue上的$emit
和$on
。
其实这么解释一下,感觉原本一开始看到的发布-订阅模式一下子就清晰起来了。
生活中的发布-订阅模式
最常见的就是手机的短信。比较符合的如没有消费账单、来电提醒。有些广告这些,大概也算吧。
再比如说公司的产品售出,有些买家会留下他们的手机号,然后售货员答应他,当这个产品出新的版本时会及时通知他们。
生活中的应用非常广泛,不详细介绍了。
发布-订阅模式的作用
将上面的运营商和用户,商家和买家分别看作是一个对象,其实也可以看出,发布-订阅是一种对象间的一(发布者)对多(订阅者)的松耦合关系,当一个对象状态发生变化时,所有依赖它的其他对象都会得到状态改变的通知。
1、广泛应用于异步编程中,且替换了传递回调函数的方案
2、对象之间松耦合的相互通信
代码上的使用
源生的事件
以jq为例,下面的click便是源生的事件
$("#div").bind("click", function () {
alert("我被点击啦~");
});
$("#div").click(); //模拟用户点击
复制代码
这里需要监控用户的点击动作,但却没法预知用户在什么时候进行操作,所以我们订阅了$("#div)
(发布者)上面的点击事件,当该dom元素被点击时,它会向订阅者发布这个消息。
在代码上,我们可以随意增加或者删除订阅者。
自定义事件
很多情况下,我们会使用一些自定义事件。
//发布者对象
let com = {};
//订阅者缓存列表,为什么是对象不是数组?因为订阅者都有可以分类。
//比如买家有买手机的,有买电脑的,想要手机的总不能电脑新款发布就也给他推消息吧
com.consumlist = {};
com.on = function(key, fn) {
//比如之前只有手机,后来有个新的客户需要订阅电脑的,那么需要新增电脑分类
if (!this.consumlist[key]) {
this.consumlist[key] = [];
}
// 把函数添加到对应key的缓存列表里
this.consumlist[key].push(fn);
}
com.emit = function() {
// 第一个参数是对应的类
let key = [].shift.call(arguments),
fns = this.consumlist[key];
// 如果缓存列表里没有函数就返回false
if (!fns || fns.length === 0) {
return false;
}
// 遍历key值对应的缓存列表
// 依次执行函数的方法
fns.forEach(fn => {
// arguments 发布信息是推送的参数
fn.apply(this, arguments);
});
};
// 测试用例
com.on('phone', () => {
console.log('手机有新款了告诉我');
});
com.on('computer', () => {
console.log('电脑有新款了告诉我');
});
com.emit('phone');
com.emit('computer' );
/*
手机有新款了告诉我
电脑有新款了告诉我
*/
复制代码
通用实现
比如一个客户,本来在韩国手机代理商处订阅了手机,后来又搬去了日本,那是否需要在日本再写一遍发布-订阅功能呢?有没有办法让日本和韩国的代理商都有这样的功能呢?
自然是有的。还是上面那个例子,把发布-订阅功能提取出来,放到单独的对象里。
let event = {
consumlist: {},
on(key, fn) {
if (!this.consumlist[key]) {
this.consumlist[key] = [];
}
this.consumlist[key].push(fn);
},
emit() {
let key = [].shift.call(arguments),
fns = this.consumlist[key];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach(fn => {
fn.apply(this, arguments);
});
},
remove(key, fn) {
// 取消订阅
let fns = this.consumlist[key];
// 如果key对应的消息没有被人订阅,直接返回
if (!fns) return false;
// 如果没有传对应函数的话
// 默认取消key对应的所有订阅
if (!*fn*) {
fns && (fns.length = 0);
} else {
//反向遍历订阅的回调函数列表
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1); //删除订阅者
}
});
}
}
};
// 测试用例
event.on('phone', fn1 = () => {
console.log('手机有新款了告诉我');
});
event.on('phone', fn2 = () => {
console.log('手机版本更新了告诉我');
});
event.remove('phone', fn1);
event.emit('phone');
/*
手机版本更新了告诉我
*/
复制代码
必须先订阅后发布吗
前面所说的都是发布-订阅模式,都是订阅者先订阅一个消息,随后才能接收到发布者发布的消息,如果顺序反过来,发布者在没有订阅它的对象时,发送的消息无疑是消失在宇宙中。
但是在某些情况下,我们需要保存这些消息,等到有对象订阅它时,重新把消息发布给订阅者。
以之前做过的一个项目为例,我们拿到菜单,然后获取用户权限后才能渲染菜单,才能跳转页面。获取用户权限是一个异步操作,当获取成功后会发布一个消息(跳转页面)。但是异步请求返回的时间很快,有可能菜单还没有渲染好,他就发布了消息(跳转页面)。vue的$nextick()
可以解决这个问题(仅针对Dom渲染前的发布事件),我们可以将发布事件存放到一个$nextick()
中,当获取权限完成后,因为还没有渲染菜单,订阅者(dom元素)还未被渲染完成,当菜单渲染完成后,订阅者(dom元素)渲染完成,接收到原发布信息。
总结
优点
1、时间上的解耦
2、对象之间的解耦
3、可完成更松耦合的异步编程的编写
缺点
1、创建订阅者需要消耗一定的时间和内存
2、当订阅的消息最后都未发生,这个订阅者会始终存在内存里
3、虽然弱化了对象之间的联系,但过度使用(多个发布者和订阅者嵌套在一起)时,程序难以跟踪和理解
参考
《javascript设计模式与开发实践》