1. 前言

发布订阅者模式是为了发布者和订阅者之间避免产生依赖关系,发布订阅者之间的订阅关系由一个中介列表来维护。发布者只需做好发布功能,至于订阅者是谁,订阅者做了什么事情,发布者是无需关心的

2. 什么是发布订阅模式

发布订阅:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。(节选自百度百科)

就拿公众号来说

  1. 只有该公众号的订阅者才能收到推送
  2. 公众号只负责推送信息,不关心是谁订阅了我,只要有信息推送,那么就推送给所有的订阅者
  3. 订阅者无需时不时的查看公众号是否有信息推送,只要公众号推送信息后,该订阅者就会收到通知
  4. 订阅者可随时取消对该公众号的订阅

在调用方法时首先要发布方法,确保调用方法能够正常调用到。可以向一类相同事件中添加很多方法。当调用这一类方法时,可以统一调用整个流程。

发布订阅者模式,可以让我们不再涉及更多的回调处理,而且可以使模块的颗粒度更小。比如有个ajax的数据展示,其中一个订阅者A可以只负责数据的表格展示,另一个订阅者B只负责数据总量的计算。当有需求要把数据总量的计算修改为当前页的数据总量和整体的数据总量计算,那么订阅者A是不用任何变动的!

3. 发布订阅优缺点

发布订阅模式确实为我们的代码带来最小的耦合,并不是所有场景都适合使用这种模式,这种模式也有其利弊。

  1. 支持简单的广播通信,自动通知所有已经订阅过的对象。
  2. 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  3. 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

模块之间如果用了太多的全局发布-订阅模式来通信,那么模块与模块之间的联系就被隐藏到了背后,我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其他模块调用。

4. 举例

上面说了那么多都是纸上谈兵,那么到底该订阅发布模式该如何实现呢?

class Boos {
  constructor(){
    this.peopleList = {};
  }
  add(key,fn){
    let {peopleList} = this;
    !peopleList[key] && (peopleList[key] = []);
    this.peopleList[key].push(fn);
  }
  run(...arg){
    let key = Array.prototype.shift.call(arg);
    let {peopleList} = this;
    let fns = peopleList[key];
    if(!fns && !fns.length) return false;
    fns.forEach((el) => {
      el.apply(this,arg);
    })
  }
  remove(key,fn){
    let {peopleList} = this;
    let fns = peopleList[key];
    if(!fns && !fns.length) return false;
    fns.forEach((el,index) => {
      if(el===fn) {
        fns.splice(index,1);
      }
    })
  }
}
let boos = new Boos();
let married = (name) => {
  console.log(`${name}上班`);
}
let unemployment = (name) => {
  console.log(`${name}出差`);
}
boos.add('marrgie',married)
boos.add('unemployment',unemployment)
boos.run('marrgie','张三');
boos.remove('unemployment',unemployment);
boos.run('unemployment','李四');
boos.run('marrgie','李四');

上面Boos类,可以拥有发布,执行和删除任务,boos给某个员工发布命令,让员工做他应该做的事情。无论是上班还是出差,我们不需要关系他们具体如何实现,只需要boos知道员工能做什么事情就好了。

class Boos {
  constructor(){
    this.peopleList = {};
  }
  add(key,fn){
    let {peopleList} = this;
    !peopleList[key] && (peopleList[key] = []);
    this.peopleList[key].push(fn);
  }
  run(...arg){
    let key = Array.prototype.shift.call(arg);
    let {peopleList} = this;
    let fns = peopleList[key];
    console.log(key)
    if(!fns && !fns.length) return false;
    fns.forEach((el) => {
      el.apply(this,arg);
    })
  }
  remove(key,fn){
    let {peopleList} = this;
    let fns = peopleList[key];
    if(!fns && !fns.length) return false;
    fns.forEach((el,index) => {
      if(el===fn) {
        fns.splice(index,1);
      }
    })
  }
}
class Work {
  married(name){
    console.log(`${name}上班`);
  }
  unemployment(name){
    console.log(`${name}出差`);
  }
  writing(name){
    console.log(`${name}写作`);
  }
  writeCode(name){
    console.log(`${name}打代码`);
  }
}
class Staff {
  constructor(name){
    this.name = name;
  }
  getName(){
    return this.name;
  }
}
let boos = new Boos();
let work = new Work();
let aaron = new Staff("Aaron");
let angie = new Staff("Angie");
let aaronName = aaron.getName();
let angieName = angie.getName()
boos.add(aaronName,work.married);
boos.add(aaronName,work.writing);
boos.add(aaronName,work.writeCode);
boos.add(angieName,work.unemployment);
boos.run(aaronName,aaronName);
boos.run(angieName,angieName);

上面共维护了三个类,每个类都在做自己的事情,boos可以为员工分配不同的工作,即时需要添加工作类与类之间没有任何沟耦合性。各自维护自己,任意一个类发生变化都不会互相影响。

上面简单的实现了一下发布订阅者模式的模型,在样例代码中,我们也能够看到,发布者和订阅者之间仅仅依靠订阅关系来维持,而且发布者也不用关心订阅者的内部具体是怎么实现的。

03-18 06:31