天天看點

談談觀察者模式和釋出訂閱模式

這兩個東西捋清楚需要點耐心。

1、觀察者vs釋出訂閱

  • 首先觀察者跟釋出訂閱這兩者雖然看起來很相似,但是兩者是有差異的,至少從實作方式上就有差異。
  • 其次,雖然兩者有差異,但是說釋出訂閱模式是觀察者模式的變異也是ok的,因為它們思想上是一緻的。
  • 最後,做個類比,觀察者模式類似于

    房東—租客

    ,釋出訂閱模式類似于

    房東—中介—租客

    ,本文将圍繞這兩個模型來探讨實作。
談談觀察者模式和釋出訂閱模式

觀察者vs釋出訂閱

  • 下面我們就用結合代碼來捋一捋。

2、觀察者模式

  • 既然我們把觀察者模式類比為

    房東——使用者

    的這種關系模型,那我們就來想象一下,這個房東是某拆遷戶,名下出租的房子有

    别墅(bigHouse)

    洋房(mediumHouse)

    平房(smallHouse)

    等三種戶型出租,他微信裡面有很多租客,分有錢的、沒錢的等等,這些是前提。
  • 首先我們來定義一下租客。(你先不管裡面為什麼要寫這些方法屬性,後面把這個對象執行個體化變成個人以後會闡述的,你先看,看完了再回頭review一下)
// 觀察者(租客)
class Observer {
  constructor(subject) {
    this.subject = subject;
  }
  notify() {
    console.log(`收到一條房東的消息,${this.subject}空了!!!`);
  }
}           

複制

  • 然後我們需要描述下房東:
// 主題(房東)
class Subject {
  // 根據戶型的不同收集相應的訂閱者
  constructor() {
    this.subjectList = {};
  }
  // 訂閱
  add(subject, observer) {
    if (!this.subjectList[subject]) {
      this.subjectList[subject] = [];
    }
    this.subjectList[subject].push(observer);
  }
  // 解除訂閱
  remove(subject, observer) {
    this.subjectList[subject].forEach((item, index) => {
      if (item === observer) {
        this.subjectList[subject].splice(index, 1);
      }
    });
  }
  // 觸發事件
  fire(subject) {
    this.subjectList[subject].forEach((item) => item.notify());
  }
}           

複制

  • Ok,接下來有這麼三位觀察者租客ABC,他們分别想租

    别墅(bigHouse)

    洋房(medium)

    平房(small)

// 不同的使用者想關注别墅(big)、`洋房(medium)`...等不同的戶型
const observerA = new Observer("bigHouse");
const observerB = new Observer("mediumHouse");
const observerC = new Observer("smallHouse");           

複制

  • 當ABC這三個租客向房東表達了他們的意向以後,房客加他們微信的時候就對其分了不同的組:
// 把這3個觀察者添加到相應的分組
const subjects = new Subject();
subjects.add("bigHouse", observerA);
subjects.add("mediumHouse", observerB);
subjects.add("smallHouse", observerC);           

複制

  • 某一天,别墅空了,于是房東對

    别墅組

    的小夥伴們群發了一條消息,于是你就收到了推送;又某一天,洋房空了。。。
// 某一天
subjects.fire("bigHouse"); // 收到一條房東的消息,bigHouse空了!!!
// 又某一天
subjects.fire("mediumHouse"); // 收到一條房東的消息,mediumHouse空了!!!
// 再某一天
subjects.fire("smallHouse"); // 收到一條房東的消息,smallHouse空了!!!           

複制

  • 這就是觀察者模式,完整代碼如下:
// 觀察者(租客)
class Observer {
  constructor(subject) {
    this.subject = subject;
  }
  notify() {
    console.log(`收到一條房東的消息,${this.subject}空了!!!`);
  }
}

// 主題(房東)
class Subject {
  // 根據戶型的不同收集相應的訂閱者
  constructor() {
    this.subjectList = {};
  }
  // 訂閱
  add(subject, observer) {
    if (!this.subjectList[subject]) {
      this.subjectList[subject] = [];
    }
    this.subjectList[subject].push(observer);
  }
  // 解除訂閱
  remove(subject, observer) {
    this.subjectList[subject].forEach((item, index) => {
      if (item === observer) {
        this.subjectList[subject].splice(index, 1);
      }
    });
  }
  // 觸發事件
  fire(subject) {
    this.subjectList[subject].forEach((item) => item.notify());
  }
}

// 不同的使用者想關注别墅(big)、`洋房(medium)`...等不同的戶型
const observerA = new Observer("bigHouse");
const observerB = new Observer("mediumHouse");
const observerC = new Observer("smallHouse");

// 把這3個觀察者添加到相應的分組
const subjects = new Subject();
subjects.add("bigHouse", observerA);
subjects.add("mediumHouse", observerB);
subjects.add("smallHouse", observerC);

// 某一天
subjects.fire("bigHouse"); // 收到一條房東的消息,bigHouse空了!!!
// 又某一天
subjects.fire("mediumHouse"); // 收到一條房東的消息,mediumHouse空了!!!
// 再某一天
subjects.fire("smallHouse"); // 收到一條房東的消息,smallHouse空了!!!           

複制

3、釋出訂閱模式

(1)了解

  • 當你了解了觀察者模式

    房東—租客

    這種模型以後,你會發現,如果觀察者很多,那麼房東壓力還是挺大的,比如收錢的壓力。
  • 這個時候,房東每天簽合同、收房租跑斷腿,不堪其擾,于是就去拜托中介,交給中介打理省心,于是就有了類似于

    房東—中介—租客

    的這種釋出訂閱模式。

(2)實踐

  • 首先我們需要定義一下

    中介機構

class EventBus {
  constructor() {
    // 定義事件池
    // 資料結構應該長這樣:{<事件隊列名>:<要執行的事件隊列>}
    // {'buy': ['func1','func2','func3']}
    this.events = {};
  }

  /**
   * 訂閱
   * @param {事件隊列名} name
   * @param {事件函數} func
   */
  on(name, func) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(func);
  }

  /**
   * 釋出
   * @param {事件隊列名} args
   * @param {傳入參數} args
   */
  emit(name, ...args) {
    if (!this.events[name]) {
      return;
    }
    this.events[name].forEach(item => {
      item.apply(this, args);
    });
  }

  /**
   * 删除
   * @param {事件隊列名} args
   */
  remove(name) {
    if (this.events[name]) {
      delete this.events[name];
    }
  }
}           

複制

  • 然後

    租客

    通過中介機構根據自己的需要訂閱了不同的房型。
// 執行個體化
const bus = new EventBus();

// 不同租客訂閱了不同需求房型
bus.on("bigHouse", function (value) {
  console.log(`A收到了一條消息:${value}`);
});

bus.on("mediumHouse", function (value) {
  console.log(`B收到了一條消息:${value}`);
});

bus.on("smallHouse", function (value) {
  console.log(`C收到了一條消息:${value}`);
});           

複制

  • 某一天,

    房東

    就直接通過中介發消息了,說:
// 某天房東通過中介釋出了一條消息
bus.emit("bigHouse", "别墅有房了"); // A收到了一條消息:别墅有房了
// 又某一天
bus.emit("mediumHouse", "洋房有房了"); // B收到了一條消息:洋房有房了
// 再某一天。。。。
bus.emit("smallHouse", "小平層有房了"); // C收到了一條消息:小平層有房了           

複制

  • 完整代碼:
class EventBus {
  constructor() {
    // 定義事件池
    // 資料結構應該長這樣:{<事件隊列名>:<要執行的事件隊列>}
    // {'say': ['func1','func2','func3']}
    this.events = {};
  }

  /**
   * 訂閱
   * @param {事件隊列名} name
   * @param {事件函數} func
   */
  on(name, func) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(func);
  }

  /**
   * 釋出
   * @param {事件隊列名} args
   * @param {傳入參數} args
   */
  emit(name, ...args) {
    if (!this.events[name]) {
      return;
    }
    this.events[name].forEach((item) => {
      item.apply(this, args);
    });
  }

  /**
   * 删除
   * @param {事件隊列名} args
   */
  remove(name) {
    if (this.events[name]) {
      delete this.events[name];
    }
  }
}

const bus = new EventBus();

// 不同租客訂閱了不同需求房型
bus.on("bigHouse", function (value) {
  console.log(`A收到了一條消息:${value}`);
});

bus.on("mediumHouse", function (value) {
  console.log(`B收到了一條消息:${value}`);
});

bus.on("smallHouse", function (value) {
  console.log(`C收到了一條消息:${value}`);
});

// 某天房東通過中介釋出了一條消息
bus.emit("bigHouse", "别墅有房了"); // A收到了一條消息:别墅有房了
// 又某一天
bus.emit("mediumHouse", "洋房有房了"); // B收到了一條消息:洋房有房了
// 再某一天。。。。
bus.emit("smallHouse", "小平層有房了"); // C收到了一條消息:小平層有房了           

複制

回顧一下,從這個例子就可可以看到:

  • EventBus

    這個就是相當于扮演了

    中介機構

    的角色。
  • emit釋出

    就相當于是

    房東

    。房東不定時随機的釋出消息,說某某套房子空了。
  • on訂閱

    就相當于是

    房客

    ,訂閱某個戶型的事件了以後就可以實時收到該戶型的通知。

4、小結

  • 通過上面這些示例,兩者之間最大的差別就是

    中介結構

    這一環,通過這個機構,房東和房客之間的溝通更加的順暢了,也就是兩者之間

    松耦合

    ,這樣的話,我們開發的時候可以将

    中介結構

    抽離成為了一個單獨的檔案,這樣使得業務邏輯更加清晰,維護起來也更加的差別。不管這兩者模式是相同還是不同的,這個我覺得是最主要的差別。
  • 其它的我這裡直接引用這篇文章的小結概況一下吧,我覺得這位大佬總結還是比較到位的:
    1. 在觀察者模式中,觀察者是知道Subject的,Subject一直保持對觀察者進行記錄。然而,在釋出訂閱模式中,釋出者和訂閱者不知道對方的存在。它們隻有通過消息代理進行通信。
    2. 在釋出訂閱模式中,元件是松散耦合的,正好和觀察者模式相反。
    3. 觀察者模式大多數時候是同步的,比如當事件觸發,Subject就會去調用觀察者的方法。而釋出-訂閱模式大多數時候是異步的(使用消息隊列)。
    4. 觀察者 模式需要在單個應用程式位址空間中實作,而釋出-訂閱更像交叉應用模式。
  • 當然,以上這些都是我自己的了解,歡迎交流。

參考學習:

https://juejin.im/post/5a14e9edf265da4312808d86

https://molunerfinn.com/observer-vs-pubsub-pattern

https://juejin.im/post/5bb1bb616fb9a05d2b6dccfa

https://juejin.im/post/57de12355bbb50005e648bd8