JavaScript 設(shè)計(jì)模式(中)——5.發(fā)布訂閱模式(觀察者模式)

5 發(fā)布訂閱模式(觀察者模式)

發(fā)布訂閱模式定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個(gè)對象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對象都將得到通知;

5.1 發(fā)布- 訂閱模式的作用

發(fā)布—訂閱模式的應(yīng)用都非常之廣泛,首先看一個(gè)現(xiàn)實(shí)中的例子,小明最近在看房子,到了某個(gè)售樓處之后才被告知,該樓盤的房子早已售罄,小明離開之前,把電話號碼留在了售樓處。售樓處答應(yīng)他,新樓盤一推出就馬上發(fā)信息通知小明。小紅、小強(qiáng)和小龍也是一樣,他們的電話號碼都被記在售樓處的花名冊上,新樓盤推出的時(shí)候,售樓 MM 會翻開花名冊,遍歷上面的電話號碼,依次發(fā)送一條短信來通知他們;

在這個(gè)例子中使用發(fā)布—訂閱模式有著顯而易見的優(yōu)點(diǎn):

  1. 購房者不用再天天給售樓處打電話咨詢開售時(shí)間,在合適的時(shí)間點(diǎn),售樓處作為發(fā)布者會通知這些消息訂閱者。這點(diǎn)說明發(fā)布—訂閱模式可以廣泛應(yīng)用于異步編程中,這是一種替代傳遞回調(diào)函數(shù)的方案;
  2. 購房者和售樓處之間不再強(qiáng)耦合在一起,當(dāng)有新的購房者出現(xiàn)時(shí),他只需把手機(jī)號碼留在售樓處,售樓處不關(guān)心購房者的任何情況,同時(shí)售樓處的任何變動也不會影響購買者,只要售樓處記得發(fā)短信這件事情。這點(diǎn)說明發(fā)布—訂閱模式可以取代對象之間硬編碼的通知機(jī)制,一個(gè)對象不用再顯式地調(diào)用另外一個(gè)對象的某個(gè)接口。發(fā)布—訂閱模式讓兩個(gè)對象松耦合地聯(lián)系在一起,雖然不太清楚彼此的細(xì)節(jié),但這不影響它們之間相互通信。當(dāng)有新的訂閱者出現(xiàn)時(shí),發(fā)布者的代碼不需要任何修改;同樣發(fā)布者需要改變時(shí),也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。

5.2 發(fā)布訂閱模式的實(shí)現(xiàn)

  1. 實(shí)現(xiàn)上述發(fā)布訂閱模式實(shí)例步驟:
  • 首先要指定好誰充當(dāng)發(fā)布者(比如售樓處);
  • 然后給發(fā)布者添加一個(gè)緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者(售樓處的花名冊);
  • 最后發(fā)布消息的時(shí)候,發(fā)布者會遍歷這個(gè)緩存列表,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)(遍歷花名冊,挨個(gè)發(fā)短信)。
var salesOffices = {}; // 定義售樓處
salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調(diào)函數(shù)
salesOffices.listen = function( fn ){ // 增加訂閱者
  this.clientList.push( fn ); // 訂閱的消息添加進(jìn)緩存列表
};
salesOffices.trigger = function(){ // 發(fā)布消息
  for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
  f  n.apply( this, arguments ); // (2) // arguments 是發(fā)布消息時(shí)帶上的參數(shù)
  }
};
// 測試數(shù)據(jù)
salesOffices.listen( function( price, squareMeter ){ // 小明訂閱消息
  console.log( '價(jià)格= ' + price );
  console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.listen( function( price, squareMeter ){ // 小紅訂閱消息
  console.log( '價(jià)格= ' + price );
  console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 輸出: 200 萬, 88 平方米
salesOffices.trigger( 3000000, 110 ); // 輸出: 300 萬, 110 平方米
  1. 發(fā)布訂閱模式的通用實(shí)現(xiàn)
// 第一步:把發(fā)布訂閱的功能提取出來,放在一個(gè)單獨(dú)的對象內(nèi)
var event = {
  clientList: [],
  listen: function( key, fn ){
    if ( !this.clientList[ key ] ){
      this.clientList[ key ] = [];
    }
    this.clientList[ key ].push( fn ); // 訂閱的消息添加進(jìn)緩存列表
  },
  trigger: function(){
    var key = Array.prototype.shift.call( arguments ), // (1);
    fns = this.clientList[ key ];
    if ( !fns || fns.length === 0 ){ // 如果沒有綁定對應(yīng)的消息
      return false;
    }
    for( var i = 0, fn; fn = fns[ i++ ]; ){
    fn.apply( this, arguments ); // (2) // arguments 是 trigger 時(shí)帶上的參數(shù)
    }
  }
};
// 第二步:定義 installEvent 函數(shù)給所有的對象都動態(tài)安裝發(fā)布—訂閱功能
var installEvent = function( obj ){
  for ( var i in event ){
    obj[ i ] = event[ i ];
  }
};
// 測試:給售樓處對象 salesOffices 動態(tài)增加發(fā)布—訂閱功能
var salesOffices = {};
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', function( price ){ // 小明訂閱消息
  console.log( '價(jià)格= ' + price );
});
salesOffices.listen( 'squareMeter100', function( price ){ // 小紅訂閱消息
  console.log( '價(jià)格= ' + price );
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 輸出: 2000000
salesOffices.trigger( 'squareMeter100', 3000000 ); // 輸出: 3000000

5.3 取消訂閱的事件

有時(shí)也許需要取消訂閱事件的功能,因此給 event 對象增加 remove 方法,如下:

event.remove = function( key, fn ){
  var fns = this.clientList[ key ];
  if ( !fns ){ // 如果 key 對應(yīng)的消息沒有被人訂閱,則直接返回
    return false;
  }
  if ( !fn ){ // 如果沒有傳入具體的回調(diào)函數(shù),表示需要取消 key 對應(yīng)消息的所有訂閱
    fns && ( fns.length = 0 );
  }else{
    for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍歷訂閱的回調(diào)函數(shù)列表
      var _fn = fns[ l ];
      if ( _fn === fn ){
        fns.splice( l, 1 ); // 刪除訂閱者的回調(diào)函數(shù)
      }
    }
  }
};
var salesOffices = {};
var installEvent = function( obj ){
  for ( var i in event ){
    obj[ i ] = event[ i ];
  }
}
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', fn1 = function( price ){ // 小明訂閱消息
console.log( '價(jià)格= ' + price );
});
salesOffices.listen( 'squareMeter88', fn2 = function( price ){ // 小紅訂閱消息
console.log( '價(jià)格= ' + price );
});
salesOffices.remove( 'squareMeter88', fn1 ); // 刪除小明的訂閱
salesOffices.trigger( 'squareMeter88', 2000000 ); // 輸出: 2000000

5.4 網(wǎng)站登錄實(shí)例(P116)

5.5 全局的發(fā)布訂閱對象

剛剛實(shí)現(xiàn)的發(fā)布訂閱模式,給售樓處對象和登錄對象都添加了訂閱和發(fā)布的功能,這里還存在兩個(gè)小問題:

  1. 給每個(gè)發(fā)布者對象都添加了 listentrigger 方法,以及一個(gè)緩存列表 clientList ,這其實(shí)是一種資源浪費(fèi);
  2. 訂閱者跟售樓處對象還是存在一定的耦合性,訂閱者至少要知道售樓處對象的名字是 salesOffices ,才能順利的訂閱到事件;

實(shí)際上,訂閱者沒必要親自去售樓處,只需要把訂閱的請求交給中介公司,而各大房產(chǎn)公司也只需要通過中介公司來發(fā)布房子信息。為了保證訂閱者和發(fā)布者能順利通信,訂閱者和發(fā)布者都必須知道這個(gè)中介公司。因此,發(fā)布—訂閱模式可以用一個(gè)全局的 Event 對象來實(shí)現(xiàn),訂閱者不需要了解消息來自哪個(gè)發(fā)布者,發(fā)布者也不知道消息會推送給哪些訂閱者, Event 作為一個(gè)類似“中介者”的角色,把訂閱者和發(fā)布者聯(lián)系起來。見如下代碼:

var Event = (function(){
  var clientList = {}, listen, trigger, remove;
  listen = function( key, fn ){
    if ( !clientList[ key ] ){
      clientList[ key ] = [];
    }
    clientList[ key ].push( fn );
  };
  trigger = function(){
    var key = Array.prototype.shift.call( arguments ),
    fns = clientList[ key ];
    if ( !fns || fns.length === 0 ){
      return false;
    }
    for( var i = 0, fn; fn = fns[ i++ ]; ){
      fn.apply( this, arguments );
    }
  };
  remove = function( key, fn ){
    var fns = clientList[ key ];
    if ( !fns ){
      return false;
    }
    if ( !fn ){
      fns && ( fns.length = 0 );
    }else{
      for ( var l = fns.length - 1; l >=0; l-- ){
        var _fn = fns[ l ];
        if ( _fn === fn ){
          fns.splice( l, 1 );
        }
      }
    }
  };
  return {
    listen: listen,
    trigger: trigger,
    remove: remove
  }
})();
Event.listen( 'squareMeter88', function( price ){ // 小紅訂閱消息
  console.log( '價(jià)格= ' + price ); // 輸出: '價(jià)格=2000000'
});
Event.trigger( 'squareMeter88', 2000000 ); // 售樓處發(fā)布消息

5.6 模塊間通信

我們利用上一節(jié)中實(shí)現(xiàn)的發(fā)布訂閱模式的全局 Event 對象可以在兩個(gè)封裝良好的模塊中進(jìn)行通信,而這兩個(gè)模塊可以完全不知道對方的存在。比如現(xiàn)在有兩個(gè)模塊, a 模塊里面有一個(gè)按鈕,每次點(diǎn)擊按鈕之后, b 模塊里的 div 中會顯示按鈕的總點(diǎn)擊次數(shù),我們用全局發(fā)布—訂閱模式完成下面的代碼,使得 a 模塊和 b 模塊可以在保持封裝性的前提下進(jìn)行通信。

<!DOCTYPE html>
<html>
  <body>
    <button id="count">點(diǎn)我</button>
    <div id="show"></div>
  </body>
  <script type="text/JavaScript">
    var a = (function(){
      var count = 0;
      var button = document.getElementById( 'count' );
      button.onclick = function(){
        Event.trigger( 'add', count++ );
      }
    })();
    var b = (function(){
      var div = document.getElementById( 'show' );
      Event.listen( 'add', function( count ){
        div.innerHTML = count;
      });
    })();
  </script>
</html>

5.7 JavaScript 實(shí)現(xiàn)發(fā)布訂閱模式的便利性

在 JavaScript 中,無需去選擇使用推模型還是拉模型。推模型是指在事件發(fā)生時(shí),發(fā)布者一次性把所有更改的狀態(tài)和數(shù)據(jù)都推送給訂閱者。拉模型不同的地方是,發(fā)布者僅僅通知訂閱者事件已經(jīng)發(fā)生了,此外發(fā)布者要提供一些公開的接口供訂閱者來主動拉取數(shù)據(jù)。拉模型的好處是可以讓訂閱者“按需獲取”,但同時(shí)有可能讓發(fā)布者變成一個(gè)“門戶大開”的對象,同時(shí)增加了代碼量和復(fù)雜度。剛好在 JavaScript 中, arguments 可以很方便地表示參數(shù)列表,所以我們一般都會選擇推模型,使用 Function.prototype.apply 方法把所有參數(shù)都推送給訂閱者。

5.8 發(fā)布訂閱模式小結(jié)

發(fā)布訂閱模式是一種非常重要的模式,在實(shí)際開發(fā)中非常有用。既可以用在異步編程中,也可以幫助完成更松耦合的代碼編寫。發(fā)布訂閱模式還可以用來幫助實(shí)現(xiàn)一些別的設(shè)計(jì)模式,比如中介者模式。 從架構(gòu)上來看,無論是 MVC 還是 MVVM,都少不了發(fā)布—訂閱模式的參與,而且 JavaScript 本身也是一門基于事件驅(qū)動的語言。

發(fā)布訂閱模式的優(yōu)點(diǎn)一為時(shí)間上的解耦,二為對象之間的解耦。發(fā)布訂閱模式缺點(diǎn)就是創(chuàng)建訂閱者本身要消耗一定的時(shí)間和內(nèi)存,若訂閱一個(gè)消息后,也許此消息最后都未發(fā)生,但這個(gè)訂閱者會始終存在于內(nèi)存中;若過度使用的話,對象和對象之間的必要聯(lián)系也將被深埋在背后,會導(dǎo)致程序難以跟蹤維護(hù)和理解。

系列鏈接

  1. JavaScript 設(shè)計(jì)模式(上)——基礎(chǔ)知識
  2. JavaScript 設(shè)計(jì)模式(中)——1.單例模式
  3. JavaScript 設(shè)計(jì)模式(中)——2.策略模式
  4. JavaScript 設(shè)計(jì)模式(中)——3.代理模式
  5. JavaScript 設(shè)計(jì)模式(中)——4.迭代器模式
  6. JavaScript 設(shè)計(jì)模式(中)——5.發(fā)布訂閱模式
  7. JavaScript 設(shè)計(jì)模式(中)——6.命令模式
  8. JavaScript 設(shè)計(jì)模式(中)——7.組合模式
  9. JavaScript 設(shè)計(jì)模式(中)——8.模板方法模式
  10. JavaScript 設(shè)計(jì)模式(中)——9.享元模式
  11. JavaScript 設(shè)計(jì)模式(中)——10.職責(zé)鏈模式
  12. JavaScript 設(shè)計(jì)模式(中)——11. 中介者模式
  13. JavaScript 設(shè)計(jì)模式(中)——12. 裝飾者模式
  14. JavaScript 設(shè)計(jì)模式(中)——13.狀態(tài)模式
  15. JavaScript 設(shè)計(jì)模式(中)——14.適配器模式
  16. JavaScript 設(shè)計(jì)模式(下)——設(shè)計(jì)原則
  17. JavaScript 設(shè)計(jì)模式練習(xí)代碼

本文主要參考了《JavaScript設(shè)計(jì)模式和開發(fā)實(shí)踐》一書

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容