JavaScript之設計模式

發佈訂閱模式

介紹

發佈訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。在 JavaScript 開發中,咱們通常用事件模型 來替代傳統的發佈—訂閱模式。es6

異步
發佈訂閱模式能夠普遍應用於異步編程中,這是一種替代傳遞迴調函數的方案。 好比,咱們能夠訂閱 ajax 請求的 error、succ 等事件。 或者若是想在動畫的每一幀完成以後作一些事情,那咱們能夠訂閱一個事件,而後在動畫的每一幀完成以後發佈這個事件。在異步編程中,使用發佈—訂閱模式,咱們就無需過多關注對象在異步運行期間的內部狀態,而只須要訂閱感興趣的事件發生點。ajax

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

建立一個觀察者

組成部分

觀察者(或者發佈者)本質上是一個對象,它有一個屬性,做爲存放事件的容器,和三個方法,分別是訂閱消息,取消訂閱消息和發佈消息。設計模式

var observer = {
    events: {}, // 按消息類型存放事件,屬性名是消息類型,屬性值都是數組
    listen: function(){}, //訂閱,按消息類型將事件推入到事件容器中
    emit: function(){}, // 發佈,按消息類型將訂閱者訂閱的消息一次性執行完
    remove: function(){}, // 取消訂閱,按消息類型將訂閱者訂閱的事件從事件容器中移除
}

存在性校驗

var observer = {
    events: {},
    listen: function(type, cb){
      if(!this.events[type]){
        this.events[type] = [];
      }
      this.events[type].push(cb);
    },
    emit: function(type){
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = 0, task; task = this.messages[type][i++];){
          task();
        }
      }
    },
    remove: function(type, fn){
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = this.messages[type].length - 1;i>=0;i--){
          if(this.messages[type][i] === fn){
            this.messages[type].splice(i, 1);
            break;
          }
        }
      }
    },
}

發佈者給訂閱者傳遞參數

發佈者(經過emit發佈函數)須要傳遞參數給訂閱者(的回調函數),能夠將須要傳遞的參數,以一個對象的形式,做爲emit函數的第二個參數,也能夠(在emit發佈函內部)經過arguments對象獲取。數組

var observer = {
    events: {},
    listen: function(type, cb){
      if(!this.events[type]){
        this.events[type] = [];
      }
      this.events[type].push(cb);
    },
    emit: function(type, data){
      // let type = Array.prototype.shift.call(arguments);
      // let args = arguments;
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = 0, task; task = this.messages[type][i++];){
          task.call(null, data);
          // task.apply(null, args);
        }
      }
    },
    remove: function(type, fn){
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = this.messages[type].length - 1;i>=0;i--){
          if(this.messages[type][i] === fn){
            this.messages[type].splice(i, 1);
            break;
          }
        }
      }
    },
}

訂閱者給本身的回調函數綁定this

訂閱者也能夠給本身的回調函數綁定this服務器

var observer = {
    events: {},
    listen: function(type, cb, self){
      if(!this.events[type]){
        this.events[type] = [];
      }
      this.events[type].push([cb, self]);
    },
    emit: function(type, data){
      // let type = Array.prototype.shift.call(arguments);
      // let args = arguments;
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = 0, task; task = this.messages[type][i++];){
          task[0].call(task[1], data);
          // task[0].apply(task[1], args);
        }
      }
    },
    remove: function(type, fn){
      if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){
        for(let i = this.messages[type].length - 1;i>=0;i--){
          if(this.messages[type][i][0] === fn){
            this.messages[type].splice(i, 1);
            break;
          }
        }
      }
    },
}

觀察者的鏈式調用

能夠將訂閱,發佈,以及取消訂閱三個方法返回this,來實現觀察者對象的鏈式調用,由於都是經過觀察者來調用這個三個方法,所以這三個方法的this都是這個觀察者。閉包

var observer = {
    events: {},
    listen: function(){ 
      ...
      return this;
    },
    emit: function(){
      ...
      return this;
    },
    remove: function(){
      ...
      return this;
    },
}

其餘形式

由於觀察者本質上是一個對象,所以咱們能夠把它封裝成一個類(構造函數或es6的class);也能夠把它單獨放在一個文件裏,而後導出一個對象(注意events做爲私有屬性,只導出三個方法便可);還能夠做爲一個IIFE(閉包),導出一個對象(注意events做爲私有屬性,只導出三個方法便可)。app

class Observer {
  constructor() {
   this.events = {};
  }
  listen(){}
  emit(){}
  remove(){}
}
var new Observer() {
   this.events = {};
}
Observer.prototype.listen = function(){};
Observer.prototype.emit = function(){};
Observer.prototype.remove = function(){};
// event.js文件
let events = {};
function listen(){};
function emit(){};
function remove(){};

export.listen;
export.emit;
export.remove;
// 將觀察者放在閉包中,當頁面加載就當即執行
var observer = (function(){
  // 防止消息隊列暴露而被串改,故將消息容器做爲靜態私有變量保存 
  let events = {};
  return {
    listen: function(){},
    emit: function(){},
    remove: function(){},
  };
})()

必須先訂閱再發布嗎

咱們所瞭解到的發佈—訂閱模式,都是訂閱者必須先訂閱一個消息,隨後才能接收到發佈者 發佈的消息。若是把順序反過來,發佈者先發布一條消息,而在此以前並無對象來訂閱它,這 條消息無疑將消失在宇宙中。異步

在某些狀況下,咱們須要先將這條消息保存下來,等到有對象來訂閱它的時候,再從新把消 息發佈給訂閱者。就如同 QQ 中的離線消息同樣,離線消息被保存在服務器中,接收人下次登陸 上線以後,能夠從新收到這條消息。模塊化

這種需求在實際項目中是存在的,好比在以前的商城網站中,獲取到用戶信息以後才能渲染 用戶導航模塊,而獲取用戶信息的操做是一個 ajax 異步請求。當 ajax 請求成功返回以後會發布 一個事件,在此以前訂閱了此事件的用戶導航模塊能夠接收到這些用戶信息。

可是這只是理想的情況,由於異步的緣由,咱們不能保證 ajax 請求返回的時間,有時候它返 回得比較快,而此時用戶導航模塊的代碼尚未加載好(尚未訂閱相應事件),特別是在用了 一些模塊化惰性加載的技術後,這是極可能發生的事情。也許咱們還須要一個方案,使得咱們的 發佈—訂閱對象擁有先發布後訂閱的能力。

爲了知足這個需求,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裝函數將被存入堆棧中,等到終於有對象來訂閱此事件的時候,咱們將遍歷堆棧而且依次執行這些包裝函數, 也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像 QQ 的未讀消息只會被重 新閱讀一次,因此剛纔的操做咱們只能進行一次。

咱們須要實現這樣的邏輯

observer.emit('click', {a:1});
observer.listen('click',(args)=>{console.log(args)}) // {a:1}

目標明確後, 來着手實現它:

var observer = {
  events: {},
  cacheList: [],
  listen: function(type, cb){
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(cb)
    for (let i = 0; i < this.cacheList.length; i++) {
      this.cacheList[i]();
    }
  },
  emit: function(type, data){
    const that = this
    function cache() {
      let arr = that.events[type];
      for (let i = 0; i < arr.length; i++) {
        arr[i].call(arr[i], data);
      }
    }
    this.cacheList.push(cache)
  },
}

存疑:保證只執行了一次?

參考文獻
JavaScript 中常見設計模式整理

相關文章
相關標籤/搜索