js - 觀察者模式與訂閱發佈模式

零.序言

轉載&參考:

1.JavaScript 設計模式系列 - 觀察者模式javascript

2.JavaScript 設計模式(六):觀察者模式與發佈訂閱模式css

 

1、觀察者模式(observer)

概要:

觀察者模式:定義了對象間一種一對多的依賴關係,當目標對象 Subject 的狀態發生改變時,全部依賴它的對象 Observer 都會獲得通知。html

  這種模式的實質就是咱們能夠對某個對象的狀態進行觀察,而且在發生改變時獲得通知(以進一步作出相應的行爲)。前端

 

  這種模式在日常日用中很常見,好比咱們監聽 div 的 click 事件,其本質就是觀察者模式。 div.addEventListener('click', function(e) {...}) ,用文字描述:觀察 div 對象,當它被點擊了(發生變化),執行匿名函數(接受通知,而後作出相應行爲)。java

 

模式特徵以及角色:

  1. 一個目標者對象 Subject,擁有方法:添加 / 刪除 / 通知 Observerjquery

  2. 多個觀察者對象 Observer,擁有方法:接收 Subject 狀態變動通知並處理;npm

  3. 目標對象 Subject 狀態變動時,通知全部 Observerbootstrap

Subject 能夠添加一系列 Observer, Subject 負責維護與這些 Observer 之間的聯繫,「你對我有興趣,我更新就會通知你」。後端

 

  Subject - 被觀察者,發佈者;設計模式

  Observer - 觀察者,訂閱者;

 

示例:

  爲加深理解,以具體實例來看看:現有三個報社,報社1、2、三;有兩個訂報人,訂閱者1,訂閱者2。此處,報社就是被觀察者、訂閱人就是觀察者。

 

被觀察者: - 主要功能是維護訂閱本身的人以及分發消息

var Publish = function(name) {
  this.name = name;
  this.subscribers = []; // 數組中存放全部的訂閱者,本例中是所表明的觀察者的行爲
}

// 分發,發佈消息
Publish.prototype.deliver = function (news) {
  var publish = this; // 各報社實例
  // 通知全部的訂閱者
  this.subscribers.forEach(item => {
    item(news, publish); // 每一個訂閱者都收到了 news, 而且還知道是哪家報社發佈的
  })
  return this; // 方便鏈式調用
}

 

觀察者: - 主要功能是(主動)訂閱或取消訂閱報社

// 訂閱
Function.prototype.subscribe = function(publish) {
  var sub = this; // 當前訂閱者這我的
  // 1. publish.subscribers 中,名字可能重複
  // 2. publish.subscribers 數組裏面已有的人,不能再次訂閱
  var alreadyExists = publish.subscribers.some(function(item) {
    return item === sub;
  })
  // 若是出版社名單中沒有這我的,則加入進去
  if (!alreadyExists) publish.subscribers.push(sub);
  
  return this; // 方便鏈式調用
}


// 取消訂閱
Function.prototype.unsubscribe = function(publish) {
  var sub = this;
  // filter (過濾函數:循環便利數組的每個元素,執行一個函數若是不匹配,則刪除該元素)
  publish.subscribers = publish.subscribers.filter(function(item){
    return item !== sub ;
  });
  return this; // 方便鏈式調用
}

 

以上所用的準備工做都已經作完了,接下來具體將具體的demo:

 

// 實例化發佈者對象(報社)
var pub1 = new Publish('報社一');
var pub2 = new Publish('報社二');
var pub3 = new Publish('報社三');

// 定義觀察者,當報社有了新的消息後,觀察者會收到通知
// 本例中以觀察者的行爲代替觀察者對象,模擬 addEventListener
var sub1 = function (news, pub) {
  console.log(arguments);
  document.getElementById('sub1').innerHTML += pub.name + news + '\n';
}

var sub2 = function (news, pub) {
  console.log(arguments);
  document.getElementById('sub2').innerHTML += pub.name + news + '\n';
}

// 執行訂閱方法,這一步是觀察者主動
sub1.subscribe(pub1).subscribe(pub2);
sub2.subscribe(pub1).subscribe(pub2).subscribe(pub3);

--------------------- 分割線 ---------------------
var p1 = document.getElementById('pub1');  // dom
var p2 = document.getElementById('pub2');  // dom 
var p3 = document.getElementById('pub3');  // dom

// 事件綁定, 觸發 報社 的消息分發
p1.onclick = function() {
  pub1.deliver(document.getElementById('text1').value, pub1);
}

p2.onclick = function() {
  pub2.deliver(document.getElementById('text2').value, pub2);
}

p3.onclick = function() {
  pub3.deliver(document.getElementById('text3').value, pub3);
}

 

其餘資源部分:

 1 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
 2 <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
 3 <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
 4 
 5 <div class="col-lg-8">
 6   <div class="input-group">
 7     <span id="pub1" class="input-group-addon">
 8       報社一
 9     </span>
10     <input v-model="user.name" type="text" class="form-control"  id="text1">
11   </div>
12 </div>
13 <div class="col-lg-8">
14   <div class="input-group">
15     <span id="pub2" class="input-group-addon">
16       報社二
17     </span>
18     <input v-model="user.name" type="text" class="form-control"  id="text2">
19   </div>
20 </div>
21 <div class="col-lg-8">
22   <div class="input-group">
23     <span id="pub3" class="input-group-addon">
24       報社三
25     </span>
26       <input v-model="user.name" type="text" class="form-control"  id="text3">
27   </div>
28 </div>
29   
30 <div class="col-lg-8">
31   訂閱者一
32   <textarea id="sub1" class="form-control" rows="5"></textarea>
33   訂閱者二:
34   <textarea id="sub2"class="form-control" rows="5"></textarea>
35 </div>
html 結構

 

姿式分解:

  1.分割線之上是報社、訂閱者的實例化,以及訂閱者訂閱報社,如同咱們在日常代碼中自定義 click 事件同樣;

  2.分割線之下則是普通的添加 click 事件,這裏面須要注意的是咱們利用本身實現的 deliver 函數完成消息分發(通知)功能;

  3.點擊「報社一」,兩個訂閱者都收到了通知,執行對應的行爲,點擊」報社三「,由於只有訂閱者2 訂閱了這家報社,故只有訂閱者2 收到消息並完成行爲;

 

 

另外一個栗子:

// 目標者
class Subject {
  constructor() {
    this. observers = []; // 觀察者列表
  }

  // 添加訂閱者
  add(observer) {
    this.observers.push(observer);
  }

  // 刪除...
  remove(observer) {
    let idx = this.observers.findIndex(item => item === observer);
    idx > -1 && this.observers.splice(idx, 1);
  }

  // 通知
  notify() {
    for(let o of this.observers) {
      o.update();
    }
  }
}

// 觀察者
class Observer {
  constructor(name) {
    this.name = name;
  }

  // 目標對象更新時觸發的回調,即收到更新通知後的回調
  update() {
    console.log(`目標者通知我更新了,我是:${this.name}`);
  }
}

// 實例化目標者
let subject = new Subject();

// 實例化兩個觀察者
let obs1 = new Observer('前端');
let obs2 = new Observer('後端');

// 向目標者添加觀察者
subject.add(obs1);
subject.add(obs2);

subject.notify();

 

優缺點:

  優勢明顯:下降耦合,二者都專一於自身功能;

  缺點也很明顯:全部觀察者都能收到通知,沒法過濾篩選;

 

2、發佈訂閱模式(Publisher && Subscriber)

概要:

發佈訂閱模式:基於一個事件(主題)通道,但願接收通知的對象 Subscriber 經過自定義事件訂閱主題,被激活事件的對象 Publisher 經過發佈主題事件的方式通知各個訂閱該主題的 Subscriber 對象。

 

  發佈訂閱模式與觀察者模式的不一樣,「第三者」 (事件中心)出現。目標對象並不直接通知觀察者,而是經過事件中心來派發通知。

 

代碼實現

// 控制中心
let pubSub = {
  list: {},

  // 訂閱
  subscribe: function(key, fn) {
    if (!this.list[key]) this.list[key] = [];

    this.list[key].push(fn);
  },

  //取消訂閱
  unsubscribe: function(key, fn) {
    let fnList = this.list[key];

    if (!fnList) return false;

    if (!fn) { // 不傳入指定的方法,清空所用 key 下的訂閱
      fnList && (fnList.length = 0);
    } else {
      fnList.forEach((item, index) => {
        item === fn && fnList.splice(index, 1);
      });
    }
  },

  // 發佈
  publish: function(key, ...args) {
    for (let fn of this.list[key]) fn.call(this, ...args);
  }
}

// 訂閱
pubSub.subscribe('onwork', time => {
  console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
  console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
  console.log(`吃飯了:${time}`);
})

pubSub.subscribe('onwork', work => {
  console.log(`上班了:${work}`);
})

// 發佈
pubSub.publish('offwork', '18:00:00'); 
pubSub.publish('launch', '12:00:00');

// 取消訂閱
pubSub.unsubscribe('onwork');

 

其實,嚴格來說 DOM 的事件監聽是「發佈訂閱模式」:

let loginBtn = document.getElementById('#loginBtn');

// 監聽回調函數(指定事件)
function notifyClick() {
    console.log('我被點擊了');
}

// 添加事件監聽
loginBtn.addEventListener('click', notifyClick);
// 觸發點擊, 事件中心派發指定事件
loginBtn.click();             

// 取消事件監聽
loginBtn.removeEventListener('click', notifyClick);

 

優缺點:

  優勢:解耦更好,細粒度更容易掌控;

  缺點:不易閱讀,額外對象建立,消耗時間和內存(不少設計模式的通病)

 

3、兩種模式的關聯和區別

發佈訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。

  1. 觀察者模式維護單一事件對應多個依賴該事件的對象關係;

  2. 發佈訂閱維護多個事件(主題)及依賴各事件(主題)的對象之間的關係;

  3. 觀察者模式是目標對象直接觸發通知(所有通知),觀察對象被迫接收通知。發佈訂閱模式多了箇中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的對象);

  4. 觀察者模式對象間依賴關係較強,發佈訂閱模式中對象之間實現真正的解耦。

相關文章
相關標籤/搜索