JavaScript設計模式之----原生JS實現簡單的發布訂閱模式


 第一部分: 發布訂閱模式簡介

發布—訂閱模式又叫觀察者模式,它定義對象間的一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。在javascript開發中,一般用事件模型來替代傳統的發布—訂閱模式。

 

發布—訂閱模式可以廣泛應用於異步編程中,是一種替代傳遞回調函數的方案。比如,可以訂閱ajax請求的error、success等事件。或者如果想在動畫的每一幀完成之后做一些事情,可以訂閱一個事件,然后在動畫的每一幀完成之后發布這個事件。在異步編程中使用發布—訂閱模式,就無需過多關注對象在異步運行期間的內部狀態,而只需要訂閱感興趣的事件發生點

第二部分:發布訂閱模式在DOM編程操作過程中的使用

  發布—訂閱模式可以取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另外一個對象的某個接口。發布—訂閱模式讓兩個對象松耦合地聯系在一起,雖然不太清楚彼此的細節,但這不影響它們之間相互通信。當有新的訂閱者出現時,發布者的代碼不需要任何修改;同樣發布者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們

實際上,前端開發中常用的,在DOM節點上面綁定事件函數,就屬於發布—訂閱模式

document.body.addEventListener('click',function(){
  alert(2);
},false);
document.body.click();

如果需要監控用戶點擊document.body的動作,但是沒辦法預知用戶將在什么時候點擊。所以訂閱document.body上的click事件,當body節點被點擊時,body節點便會向訂閱者發布這個消息。

同理,其實DOM中的很多事件操作都是采用的這個原理

document.body.addEventListener('keyup',function(){
  alert(2);
},false);
document.body.keyup();

document.body.addEventListener('mousedown',function(){
  alert(2);
},false);
document.body.mousedown();

還可以為單個事件添加多個監聽功能,

document.body.addEventListener('click',function(){
  console.log(2);
},false);
document.body.addEventListener('click',function(){
 console.log(3);
},false);
document.body.addEventListener('click',function(){
  console.log(4);
},false);
document.body.click();    //模擬用戶點擊

// 2, 3, 4

 

第三部分:發布訂閱模式的其他使用場景

假設有一個店鋪,出售Iphone,會有多種型號的Iphone出售,而消費者也會有不同的需求,如果每個消費者都要來向店員詢問自己需要的款型的價格,那么這是一個很低效的行為,因為消費者最關心的就是型號和價格,這樣用發布訂閱模式就最合適不過了

const  shop = {}; // 首先定義一個商鋪

shop.list = [];  // 定義商鋪里的商品信息列表

shop.listen = function(fn) { // 添加訂閱者
    this.list.push(fn); // 將訂閱的商品添加進入商品心里列表
}  

shop.sell = function(){
    for( var i = 0, fn; fn = this.list[ i++ ]; ){
        fn.apply( this, arguments ); // (2) // arguments 是發布消息參數
    }
}

// 這是來了一個顧客詢問手機的價格,那么
shop.listen(function(iphone, price) {
    console.log('手機型號' + iphone);
    console.log('價格' + price)
})

// 發布消息,本店賣IphoneX, 價格7000
shop.sell('IphoneX', 7000);

shop.sell('Iphone11', 9000);

// 輸出 手機型號IphoneX, 價格7000
// 輸出 手機型號Iphone11, 價格9000

現在我們已經實現了一個最簡單的發布訂閱模式了,但這里還存在一些問題。訂閱者接收到了發布者發布的每個消息,如果我只想買Iphone11,我是不關心IphoneX的價格的,但是發布者把IphoneX的信息也推送給了我,這對我來說是不必要的困擾。所以有必要增加一個標示key,讓訂閱者只訂閱自己感興趣的消息。改寫后的代碼如下:

const  shop = {}; // 首先定義一個商鋪

shop.list = {};  // 定義商鋪里的商品信息列表

shop.listen = function(key, fn) { // 添加訂閱者
    if ( !this.list[key] ){ // 如果沒有訂閱,創建一個緩存列表
        this.list[key] = [];
    }
    this.list[key].push( fn ); // 訂閱的消息添加進消息緩存列表
}  

shop.sell = function(){
   const key = Array.prototype.shift.call( arguments );// 取出消息
   const fns = this.list[ key ]; // 取出該消息對應的回調函數集合
    if ( !fns || fns.length === 0 ){ // 如果沒有訂閱該消息,則返回
        return false;
    }
    for( var i = 0, fn; fn = fns[ i++ ]; ){
        fn.apply( this, arguments ); // (2) // arguments 是參數
    }
}

// 這是來了一個顧客詢問手機的價格,那么
shop.listen('IphoneX', function(price) {
    console.log('價格' + price)
})

// 這是來了一個顧客詢問手機的價格,那么
shop.listen('Iphone11', function(price) {
    console.log('價格' + price)
})


// 發布消息,本店賣IphoneX, 價格7000
shop.sell('IphoneX', 7000);

shop.sell('Iphone11', 9000);

// 輸出 價格7000
// 輸出 價格9000

 

依照上面的例子,我們就可以寫一個基於對象的發布訂閱的模型了

const event = {
    clientList: [],
    listen: function( key, fn ){
        if ( !this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
            this.clientList[ key ].push( fn ); // 訂閱的消息添加進緩存列表
        },
        trigger: function(){
            var key = Array.prototype.shift.call( arguments ), // (1);
            fns = this.clientList[ key ];
            if ( !fns || fns.length === 0 ){ // 如果沒有綁定對應的消息
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments ); // (2) // arguments
            }
        },
        remove: function(key, fn) {
              var fns = this.clientList[ key ];
              if ( !fns ){ // 如果key 被人訂閱,則直接返回
                 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 ); // 刪除訂閱者的回調函數
                      }
                  }
             }
        }
    };

 

// 下面是一個基於class的發布訂閱模式的模版,考慮到了邊界條件和匿名函數,屬於一個比較完整的實現

class Pubsub {
  constructor () {
  }

  list = {};

  // 添加消息監聽的方法
  subscribe (topic, func) {
    if (typeof topic !== 'string') {
      throw 'topic為字符串類型'
    }
    if (typeof func !== 'function') {
      throw 'func為函數類型'
    }
    const list = this.list;
    if (!list[topic]) {
      list[topic] = [];
    }
    list[topic].push(func);
// 為了防止匿名函數的影響,在添加時將取消監聽的方法返回 return () => this.unsubscribe(topic, func); } // 發布消息的方法 publish (topic, data) { if (typeof topic !== 'string') { throw 'topic必須是字符串類型' } const list = this.list; if(!list[topic]) { throw '不存在該事件的監聽' } else { list[topic].forEach((func)=>{ func.call(this, data) }) } } // 移除消息監聽的方法 unsubscribe (topic, func){ if(typeof topic !== 'string') { throw 'topic為字符串類型' } if(func && (typeof func !== 'function')) { throw 'func為函數類型' } const list = this.list; if(!list[topic]) { throw '不存在該topic監聽' } if(!func) { // 如果沒有第二個參數,就移除所有的監聽事件 if(list[topic]) { delete list[topic] } } else { if(!list[topic].includes(func)) { throw '要移除的事件不存在' } else { const index = list[topic].findIndex(item => item === func); list[topic].splice(index, 1); if(list[topic].length === 0) { delete list[topic] } } } } }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM