前言
终于轮到了发布订阅模式。这应该是前端开发者听到最多的设计模式了,像Vue这种MVVM框架就是在发布订阅的基础上设计的。今天我们就来简单聊一下这个模式。
说明
现在网络上有2种观点,有人认为发布—订阅模式跟观察者模式是一样的,也有人认为这是2种完全不同的设计模式。本文态度的不站边,以下内容指的都是发布订阅模式。
发布—订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
设计思路
假设现在需要一个场景,当A的状态改变之后,执行B的update方法。那我们可以马上写出以下代码:
const A = {
state:1,
setState(state){
this.state = state;
B.update();
}
}
const B = {
update(){
// ....
}
};
但是随着开发的时间增加,越来越多的实例需要根据A的状态来触发update。我们就不得不扩展A的setState方法。
const A = {
state:1,
setState(state){
this.state = state;
B.update();
C.update();
// ...
}
}
这显然不是一个好的处理方法,而发布订阅模式就可以很好的解决这个问题。优化后的代码如下:
const A = {
events:[],
state:1,
setState(state){
this.state = state;
this.trigger();
},
listen(fnc){
this.events.push(fnc);
}
remove(fnc){
this.events = this.events.filter(item=>item!==fnc);
},
trigger(){
for(let item of this.events){
item();
}
}
}
const B = {
update(){
// ....
}
};
A.listen(B.update);
可以看到我们在A加上了一个事件管理,B可以通过调用listen把事件加入A的事件管理中。然后A的状态改变之后就会遍历执行事件管理中的所有事件。
优化
当然在这套设计上还有很多可以优化的空间,如果我们可以让events是一个对象,这样就可以给事件管理加入名命空间的区分。
const A = {
events:{},
state:1,
listen(namespace,fnc){
if(!this.events[namespace])this.events[namespace] = [];
this.events[namespace].push(fnc);
}
}
还可以通过一个方法专门给一个对象添加订阅功能。
var installEvents = function(obj){
const events ={
events:[],
listen:(){},
remove(){},
trigger(){}
}
for(let i in events){
obj[i] = events[i];
}
}
installEvents(A);
JavaScript中"内置"的发布订阅
其实JavaScript默认已经内置了一套发布订阅的实现,那就是异步事件(或者说大部分以回调形式执行的内容)。无论是dom的事件监听还是js中的定时器或者网络请求回调。其实本质上也是一个发布订阅的实现。
我们在执行addEventListener来监听dom事件时,其实浏览器也帮我们做了一个事件管理,当事件触发时,会把我们传入的方法都执行一遍。
document.getElementById("myBtn").addEventListener("click", ()=>{conosle.log(1)});
document.getElementById("myBtn").addEventListener("click", ()=>{conosle.log(2)});
document.getElementById("myBtn").addEventListener("click", ()=>{conosle.log(3)});
document.getElementById("myBtn").addEventListener("click", ()=>{conosle.log(4)});
// 事件触发之后会打印出 1 2 3 4
例子场景
其实大部分需要全局回调的场景,都可以考虑用发布订阅的方式实现。这里举一个场景,就是用户登录/登出后的系统反馈。
const user = {
login(){
this.trigger('login');
},
logout(){
this.trigger('logout');
}
}
const installEvents = function(obj){
const events ={
events:[],
listen:(){},
remove(){},
trigger(){}
}
for(let i in events){
obj[i] = events[i];
}
}
installEvents(user);
// 假如页面上有一个登录按钮,他需要根据当前用户是否已经登录了来决定是否显示。
const loginButton = {
show: false,
changeState(state){
if(state === 'login'){
this.show = false;
}else{
this.show = true;
}
}
}
user.listen(loginButton.changeState);
总结
今天我们通过了解了发布订阅的概念,很幸运的是在JavaScript中其实已经存在很多内置的实现方式了。但掌握发布订阅的设计思想,可以帮助我们之后在开发中遇到需要运用的场景时,可以做出更优的选择。
参考
《JavaScript设计模式与开发实践》—— 曾探