JavaScript 設計模式之觀察者模式與發佈訂閱模式

前言

在軟體工程中,設計模式(design pattern)是對軟體設計中廣泛存在(反覆出現)的各類問題,所提出的解決方案。javascript

設計模式並不直接用來完成程式碼的編寫,而是描述在各類不一樣狀況下,要怎麼解決問題的一種方案。java

設計模式能使不穩定轉爲相對穩定、具體轉爲相對抽象,避免會引發麻煩的緊耦合,以加強軟體設計面對並適應變化的能力git

——維基百科github

設計模式是一種軟件開發的思想,有益於下降代碼的耦合性,加強代碼的健壯性。每每在大型項目中用的比較多。設計模式

今天就來介紹一下觀察者模式與發佈訂閱模式。這在解耦中很是實用。數組

什麼是觀察者模式?

先舉一個簡單的例子函數

畢業前,不少同窗都會說相似於這樣的話:post

「老王,等你結婚了,記得叫我來喝喜酒!」學習

因而有一天你真的要結婚了,且須要舉辦酒席,這時候你須要通知你的你的那些老友來喝喜酒。因而你拿起了手機給你的那些分佈於世界各地的好朋友打起了電話,說告終婚酒席一事。ui

到了酒席那天,有的朋友來了,有的人沒來禮卻到了,有的呢只有簡短的兩句祝福,剩下的只有推脫。

這就是觀察者模式

在觀察者模式中,目標與觀察者相互獨立,又相互聯繫:

  • 二者都是相互獨立的對對象個體。
  • 觀察者在目標對象中訂閱事件,目標廣播發布事件。

就像以前的例子同樣:

  • 老王就是模式中所謂的目標。
  • 同窗們在畢業前說的話就至關於在目標對象上訂閱事件。
  • 老王打電話通知朋友就是發佈事件。
  • 同窗們各自做出了不一樣的行動迴應。

這麼說咱們的代碼就慢慢創建起來了。

首先咱們須要定義兩個對象:

  1. 目標對象:Subject
  2. 觀察者對象:Observer

而且在目標對象中要存放觀察者對象的引用,就像老王要存放同窗的手機好同樣,只有存了才能聯繫嘛。因而咱們有了下面的代碼:

function Subject() {
  this.observers = new ObserverList();
}
function ObserverList() {
  this.observerList = [];
}
function Observer() {}
複製代碼

對於目標對象中的引用,咱們必須能夠動態的控制:

ObserverList.prototype.add = function(obj) {
  return this.observerList.push(obj);
};

ObserverList.prototype.count = function() {
  return this.observerList.length;
};

ObserverList.prototype.get = function(index) {
  if (index > -1 && index < this.observerList.length) {
    return this.observerList[index];
  }
};

ObserverList.prototype.indexOf = function(obj, startIndex) {
  var i = startIndex;

  while (i < this.observerList.length) {
    if (this.observerList[i] === obj) {
      return i;
    }
    i++;
  }
  return -1;
};

ObserverList.prototype.removeAt = function(index) {
  this.observerList.splice(index, 1);
};

Subject.prototype.addObserver = function(observer) {
  this.observers.add(observer);
};

Subject.prototype.removeObserver = function(observer) {
  this.observers.removeAt(this.observers.indexOf(observer, 0));
};
複製代碼

這樣咱們就能對老王手機聯繫人進行增、刪、查的操做了。

如今咱們就要考慮發佈消息的功能函數了。首先必須明確一點:目標對象並不能指定觀察者對象作出什麼相應的變化。目標對象只有通知的做用。就像老王只能告訴朋友他要辦喜酒了,至於朋友接下來怎麼辦,則全是朋友本身決定的。

因此咱們得寫一個目標廣播消息的功能函數:

Subject.prototype.notify = function(context) {
  var observerCount = this.observers.count();
  for (var i = 0; i < observerCount; i++) {
    this.observers.get(i).update(context);
  }
};
複製代碼

咱們將具體的觀察者對象該做出的變化交給了觀察者對象本身去處理。這就要求觀察者對象須要擁有本身的 update(context)方法來做出改變,同時該方法不該該寫在原型鏈上,由於每個實例化後的 Observer 對象所作的響應都是不一樣的,須要獨立存儲 update(context)方法:

function Observer() {
  this.update = function() {
    // ...
  };
}
複製代碼

到此咱們就完成了一個簡單的觀察者模式的構建。

完整代碼:

function ObserverList() {
  this.observerList = [];
}

ObserverList.prototype.add = function(obj) {
  return this.observerList.push(obj);
};

ObserverList.prototype.count = function() {
  return this.observerList.length;
};

ObserverList.prototype.get = function(index) {
  if (index > -1 && index < this.observerList.length) {
    return this.observerList[index];
  }
};

ObserverList.prototype.indexOf = function(obj, startIndex) {
  var i = startIndex;

  while (i < this.observerList.length) {
    if (this.observerList[i] === obj) {
      return i;
    }
    i++;
  }
  return -1;
};

ObserverList.prototype.removeAt = function(index) {
  this.observerList.splice(index, 1);
};

function Subject() {
  this.observers = new ObserverList();
}

Subject.prototype.addObserver = function(observer) {
  this.observers.add(observer);
};

Subject.prototype.removeObserver = function(observer) {
  this.observers.removeAt(this.observers.indexOf(observer, 0));
};

Subject.prototype.notify = function(context) {
  var observerCount = this.observers.count();
  for (var i = 0; i < observerCount; i++) {
    this.observers.get(i).update(context);
  }
};

// The Observer
function Observer() {
  this.update = function() {
    // ...
  };
}
複製代碼

什麼是發佈訂閱模式?

先舉個簡單的例子:

咱們生活中,特別是在一線城市打拼的年輕人,與租房的聯繫再密切不過了。同時咱們的身邊也有不少租房中介。

某天路人甲須要租一套三室一廳一廚一衛的房,他找到了中介問了問有沒有。中介看了看發現並無他要的房型,因而和路人甲說:「等有房東提供了此類房型的時候再聯繫你。」因而你就回去等消息了。

有一天,某一位房東將本身多餘的房屋信息以及圖片整理好發給中介,中介看了看,這不就是路人甲要的房型嗎。因而立馬打電話讓路人甲看房。最終撮合了一單生意。

這就是發佈訂閱模式

能夠看出,在發佈訂閱模式中最重要的是 Topic/Event Channel (Event)對象。咱們能夠簡單的稱之爲「中介」。

在這個中介對象中既要接受發佈者所發佈的消息,又要將消息派發給訂閱者。因此中介還應該按照不一樣的事件儲存相應的訂閱者信息。

首先咱們先會給中介對象的每一個訂閱者對象一個標識,每當有一個新的訂閱者訂閱事件的時候,咱們就給一個 subUid。

咱們先來寫一下中介對象(pubsub):

var pubsub = {};
(function(myObject) {
  var topics = {};
  var subUid = -1;

  myObject.publish = function() {};

  myObject.subscribe = function() {};

  myObject.unsubscribe = function() {};
})(pubsub);
複製代碼

這裏咱們用了工廠模式來建立咱們的中介對象。

咱們先把訂閱功能實現:

首先咱們必須認識到 topics 對象將存放着以下類型的數據:

topics = {
  topicA: [
    {
      token: subuid,
      function: func
    },
  	...
  ],
  topicB: [
    {
      token: subuid,
      function: func
    },
  	...
  ],
  ...
}
複製代碼

對於 topics 對象,存放在許多不一樣的事件名稱(topicA...),對於每個事件都有指定的一個數組對象用以存放訂閱該事件的訂閱對象及發生事件以後做出的響應。

因此當有訂閱對象在中介中訂閱事件時:

myObject.subscribe = function(topic, func) {
  //若是不存在相應事件就建立一個
  if (!topics[topic]) {
    topics[topic] = [];
  }
  //將訂閱對象信息記錄下來
  var token = (++subUid).toString();
  topics[topic].push({
    token: token,
    func: func
  });
  //返回訂閱者標識,方標在取消訂閱的時候使用
  return token;
};
複製代碼

接下來咱們來實現取消訂閱的功能:

咱們只須要遍歷 topics 各個事件中的對象便可。

myObject.unsubscribe = function(token) {
  for (var m in topics) {
    if (topics[m]) {
      for (var i = 0, j = topics[m].length; i < j; i++) {
        if (topics[m][i].token === token) {
          topics[m].splice(i, 1);
          return token;
        }
      }
    }
  }
  return this;
};
複製代碼

剩下的就是發佈事件的實現了:

咱們只須要給定事件名稱 topic 和相應的參數便可,找到相應事件所對應的訂閱者列表,遍歷調用列表中的方法。

myObject.publish = function(topic, args) {
  if (!topics[topic]) {
    return false;
  }
  var subscribers = topics[topic],
    len = subscribers ? subscribers.length : 0;
  while (len--) {
    subscribers[len].func(args);
  }
  return this;
};
複製代碼

至此,咱們的中介對象就完成了。在發佈訂閱模式中咱們沒必要在乎發佈者和訂閱者。

完整代碼:

var pubsub = {};

(function(myObject) {
  var topics = {};
  var subUid = -1;

  myObject.publish = function(topic, args) {
    if (!topics[topic]) {
      return false;
    }
    var subscribers = topics[topic],
      len = subscribers ? subscribers.length : 0;
    while (len--) {
      subscribers[len].func(args);
    }
    return this;
  };

  myObject.subscribe = function(topic, func) {
    if (!topics[topic]) {
      topics[topic] = [];
    }
    var token = (++subUid).toString();
    topics[topic].push({
      token: token,
      func: func
    });
    return token;
  };

  myObject.unsubscribe = function(token) {
    for (var m in topics) {
      if (topics[m]) {
        for (var i = 0, j = topics[m].length; i < j; i++) {
          if (topics[m][i].token === token) {
            topics[m].splice(i, 1);
            return token;
          }
        }
      }
    }
    return this;
  };
})(pubsub);
複製代碼

兩者的區別和聯繫

區別:

  1. 觀察者模式中須要觀察者對象本身定義事件發生時的相應方法。
  2. 發佈訂閱模式者在發佈對象和訂閱對象之中加了一箇中介對象。咱們不須要在意發佈者對象和訂閱者對象的內部是什麼,具體響應時間細節所有由中介對象實現。

聯繫:

  1. 兩者都下降了代碼的耦合性。
  2. 都具備消息傳遞的機制,以數據爲中心的設計思想。

實戰

這裏須要一點模板引擎的知識,關於模板引擎能夠看我以前發的一篇文章:《手擼 JavaScript 模板引擎》

假如咱們有以下模板須要渲染:

var template = `<span><% this.value %></span>`;
複製代碼

該模板依賴的數據源以下:

var data = {
  value: 0
};
複製代碼

現倘若 data 中的 value 時動態的,每隔一秒加 1。

setInterval(function() {
  data.value++;
}, 1000);
複製代碼

同時咱們也要在頁面上發生變化,這時你可能寫出以下代碼:

setInterval(function() {
  data.value++;
  document.body.innerHTML = TemplateEngine(template, data);
}, 1000);
複製代碼

咱們能夠對比一下發布訂閱模式的實現:

var template = `<span><% this.value %></span>`;
var data = {
  value: 0
};
function render() {
  document.body.innerHTML = TemplateEngine(template, data);
}
window.onload = function() {
  render();
  pubsub.subscribe("change", render);
  setInterval(function() {
    data.value++;
    pubsub.publish("change");
  }, 1000);
};
複製代碼

前者彷佛看起來很簡單明瞭,可是:

  1. 不一樣功能緊密耦合,若是之後要修改該功能,極可能牽一髮而動全身。
  2. 每每實際開發中咱們的訂閱者不止一個,發佈者的消息也不止一個,遠遠比這個例子的邏輯複雜的多。剪不斷,理還亂。

相比之下,發佈訂閱模式就顯得邏輯清晰,已於維護,值得細細體味。

值得一提:事件監聽的實現

事件監聽是咱們常常用到的功能,其實它的實現就是源自於發佈訂閱模式,不信你看:

subject.addEventListener("click", () => {
  //...
});
複製代碼

這就是在訂閱一個事件的調用。

其實觀察者模式與發佈訂閱模式與咱們息息相關!😁

-EFO-


筆者專門在 github 上建立了一個倉庫,用於記錄平時學習全棧開發中的技巧、難點、易錯點,歡迎你們點擊下方連接瀏覽。若是以爲還不錯,就請給個小星星吧!👍


2019/04/28

AJie

相關文章
相關標籤/搜索