[JS设计模式]发布—订阅模式

166 阅读3分钟

前言

终于轮到了发布订阅模式。这应该是前端开发者听到最多的设计模式了,像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设计模式与开发实践》—— 曾探