前言
对于前端同学而言,发布-订阅模式应该是最熟悉的设计模式了。平常写的最多的事件监听就是一个最好的例子。
var dom = document.querySelector('xxx');
dom.addEventListener('click', () => { //do something });
dom.click();
这里开发同学订阅这个dom对象的click事件,并且传入了一个回调函数。当这个事件被触发(用户点击),就会执行这个回调。当然,我们可以也可以订阅这个对象的其他事件,也可以多次订阅同一个事件并且传入不同的回调。
使用场景
这个设计模式的优点十分明显,他可以解耦发布者和订阅者,看一段业务中经常用到的场景。
假设一个页面需要一个接口来渲染三个模块。分别为A。B。C。当接口返回时,要调用对应模块的业务逻辑接口。
import { A, B, C } from 'dir';
getApi().then((data) => {
A.renderA();
B.renderB();
C.renderC();
});
这里有几个问题
- 假如这个getApi是你负责的模块,而A B C模块都是分别由其他人维护的,你需要侵入到模块内部,来调用他们的方法。
- 如果业务逻辑发生变化,增加一个D模块,那么此时你需要在回调里面再新增D模块,再新增的话你还得再加。其实这是不合理的,不能因为有其他模块的介入我们就得修改一次代码,这样的强耦合会造成业务逻辑的不可维护。
这个时候就可以利用一下发布-订阅模式来解决这个问题。
我们修改一下上述代码
// event.js
class Event {
constructor(){
this.watchList = {};
}
listen(key,fn){
if (this.watchList[key]) {
this.watchList[key].push(fn);
} else {
this.watchList[key] = [fn];
}
}
trigger(...args){
const key = args.shift();
if (!key) {
return;
}
const fns = this.watchList[key];
if (!fns) {
return;
}
for(let i=0;i<fns.length;i++) {
fns[i] && fns[i].apply(this,args);
}
}
}
export default new Event();
// A.js
import event from event
const A = {
renderA:() => { // do something }
};
event.listen('data-ready',() => {
A.renderA();
})
// B.js 和 C.js 同理
// page.js
import event from 'event';
getApi().then((data) => {
event.trigger('data-ready',data);
});
这么一来,不管是A B C 模块的内部方法发生了改变,还是要新增其他的模块,请求API后的回调逻辑就不用再改动了。其他模块的人只要维护好他们自己的方法就行。
写在最后
除了解耦模块和模块之间的关系以外,这个设计模式在其他领域也常被用到,比如消息中间件的异步通信中,上游不用关心下游实时返回的结果,而是等结果产生了再调用对应的事件回调。