js經典設計模式--發佈訂閱模式

什麼是發佈-訂閱模式

發佈—訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。
舉個例子,售樓處賣房,那麼售樓處要發佈房型信息,那麼它是發佈者,中介關注房型,所以中介是訂閱者,當售樓處發佈消息之後或者房型信息更新之後,中介就會收到消息。緊接着他去通知客戶。這麼做的好處是:客戶不用關心房型,不用和任何一家售樓處保持緊密的聯繫,只需要與某個中介聯繫,但是他可以通過中介知道所有房型的變化。所有客戶與售樓處是沒有耦合關係的。
如下圖:

在這裏插入圖片描述
同樣的,我們可以把它運用到編程上面來,降低代碼的耦合性。
我們試着使用發佈訂閱模式實現以上功能。

  let houseObj = {}; //定義發佈者
  houseObj.list = []; //緩存列表 (花名冊) 存放訂閱者回調函數
  //增加訂閱者
  houseObj.listen = function(fn){
  //訂閱消息添加到緩存列表
     this.list.push(fn); 
   }
  //發佈消息 是不是要遍歷這個列表
   houseObj.trigger = function(){
    for(let i = 0,fn; fn = this.list[i++];){
            fn.apply(this,arguments); // arguments 是發佈消息時附送的參數
        }
   }
  //小紅的要求 (訂閱)
  houseObj.listen(function(size){
    console.log('小紅:我要的房子是'+size+'平米');
  })

  //小綠的要求 (訂閱)
   houseObj.listen(function(size){
     console.log('小綠:我要的房子是'+size+'平米');
   })
  //執行
  houseObj.trigger(100);
  houseObj.trigger(150);

存在的問題

寫完這段代碼之後,運行一下,會發現它打印了四次,而我們理想結果是2次,因爲houseObj把所有的消息都發給某一個訂閱者了。這樣對於用戶來說是十分不友好的,他只想看他訂閱的那條消息。那我們優化一下。

  let houseObj = {}; //發佈者
  houseObj.list = {}; //緩存列表 (花名冊) 存放訂閱者回調函數

  //增加訂閱者
  houseObj.listen = function(key,fn){   //增加一個唯一標識key
  	//如果沒有訂閱過此消息 給該消息創建一個緩存列表
		(this.list[key] || (this.list[key] = [])).push(fn)
  }
  
  //發佈消息 遍歷列表
  houseObj.trigger = function(){
    //取出消息類型名稱
    let key = Array.prototype.shift.call(arguments);
    
    // 取出該消息對應的回調函數的集合
    let fns = this.list[key];
    if(!fns || fns.length === 0){
      return;
    }
    for(let i = 0,fn; fn = fns[i++];){
            fn.apply(this,arguments); // arguments 是發佈消息時附送的參數
        }
  }
  //小紅的要求 (訂閱)
  houseObj.listen('big',function(size){
    console.log('小紅:我要的房子是'+size+'平米');
  })

   //小綠的要求 (訂閱)
   houseObj.listen('small',function(size){
    console.log('小綠:我要的房子是'+size+'平米');
  })
  //執行
  houseObj.trigger('big',100);
  houseObj.trigger('small',150);

這樣我們就會看到控制檯只打印了兩次,傳了一個唯一的key。
我們知道,對於上面的代碼,對於不同的用戶去買房子這麼一個對象houseObj 進行訂閱,但是如果以後我們需要對買水果子買鞋子或者其他的對象進行訂閱呢,我們如果每次都這麼去寫是不是會很麻煩?
我們需要複製上面的代碼,再重新改下里面的對象代碼;這樣是很麻煩的。爲此我們需要進行代碼封裝 :

  // 定義一個對象
  let event = {
    list:{},
    listen: function(key,fn){   //增加一個唯一標識key
      //如果沒有訂閱過此消息 給該消息創建一個緩存列表
        (this.list[key] || (this.list[key] = [])).push(fn)
    },
   trigger: function(){
      //取出消息類型名稱
      let key = Array.prototype.shift.call(arguments);
      let fns = this.list[key];
      if(!fns || fns.length === 0){
        return;
      }
      for(let i = 0,fn; fn = fns[i++];){
        fn.apply(this,arguments); // arguments 是發佈消息時附送的參數
      }
    }
  }
  //定義一個initEvent函數,這個函數使所有的普通對象都具有發佈訂閱功能
  let initEvent = function(obj){
      for(let i in event){
        obj[i] = event[i];
      }
  };
  let houseObj = {};   //發佈者對象
  initEvent(houseObj); //爲對象添加發布-訂閱功能     

  //小明訂閱的消息
  houseObj.listen('big',function(size){
    console.log('小明:我要的房子是'+size+'平米');
  })
   //小綠訂閱的消息
   houseObj.listen('small',function(size){
    console.log('小綠:我要的房子是'+size+'平米');
  })
  houseObj.trigger('big',100);
  houseObj.trigger('small',150);

如上,我們只需要調用initEvent,便可以使所有對象都擁有發佈訂閱模式。
那麼,接下來,如果某用戶不想訂閱了

如何取消訂閱?

//刪除訂閱
  event.remove = function(key,fn){
     let fns = this.list[key];
     //如果沒有定閱過 直接返回false
     if(!fns){
       return false;
     }
     // 如果沒有傳入具體的回調函數,表示需要取消key對應消息的所有訂閱
     if(!fn){
       fn && (fns.length = 0);
     }else{
       for(let i = fns.length - 1;i >= 0; i-- ){
         let _fn = fns[i];
         _fn === fn && (fns.splice(i,1));  //刪除訂閱者對應的回調函數
       }
     }
   }
   //其餘保持不變
   //小明訂閱的第一條消息
   houseObj.listen('big',fn1 = function(size){
    console.log('小明:我要的第一套房子是'+size+'平米');
  })
   //小明訂閱的第二條消息
   houseObj.listen('big',fn2 = function(size){
    console.log('小明:我要的第二套房子是'+size+'平米');
  })
  //刪除第二條
  houseObj.remove('big',fn2);
  houseObj.trigger('big',100);

這樣,控制檯就會只有一條打印了,只有第一條消息還在。

繼續深度解耦

1.我們給每個發佈者對象都添加了 listen 和 trigger 方法,以及一個緩存列表 list,這其實是一種資源浪費。
2.小明跟售樓處對象還是存在一定的耦合性,小明至少要知道售樓處對象的名字是houseObj ,要知道房型是big,還是small或是normal才能順利的訂閱到事件。
所以我們繼續優化,封裝一個全局發佈-訂閱模式對象

封裝全局發佈-訂閱模式對象

  let Event = (function(){
    let list = {},
        listen,
        trigger,
        remove;
        listen = function(key,fn){
          (list[key] || (list[key] = [])).push(fn);
        };
        trigger = function(){
          let key = Array.prototype.shift.call(arguments),
          // 取出該消息對應的回調函數的集合
              fns = list[key];
          if(!fns || fns.length === 0){
            return false;
          }
          for(let i = 0,fn; fn = fns[i++];){
            fn.apply(this,arguments); // arguments 是發佈消息時附送的參數
          }
        };
        remove = function(key,fn){
          let fns = list[key];
          //如果沒有定閱過 直接返回false
          if(!fns){
            return false;
          }
          // 如果沒有傳入具體的回調函數,表示需要取消key對應消息的所有訂閱
          // 小明在售樓處不買了 取消了 說要買三室一廳,四室一廳,結果都是吹牛皮
          if(!fn){
            fn && (fns.length = 0);
          }else{
            for(let i = fns.length - 1;i >= 0; i-- ){
              let _fn = fns[i];
              _fn === fn && (fns.splice(i,1));  //刪除訂閱者對應的回調函數
            }
          }
        };
        return {
          listen:listen,
          trigger:trigger,
          remove:remove
        }
  })();

  Event.listen('big',function(size){
    console.log('小明想要的房型大小是'+size+'平米');
  })
  Event.trigger('big',100);

這樣,用戶連售樓處是哪都不用管了,高度解耦

FAQ

1.創建訂閱者本身要消耗一定的時間和內存,而且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於內存中。另外,發佈—訂閱模式雖然可以弱化對象之間的聯繫,但如果過度使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會導致程序難以跟蹤維護和理解。特別是有多個發佈者和訂閱者嵌套到一起的時候,要跟蹤一個 bug 不是件輕鬆的事情。
2.不要濫用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章