Javascript設計模式之發佈-訂閱模式

簡介

發佈-訂閱模式又叫作觀察者模式,他定義了一種一對多的依賴關係,即當一個對象的狀態發生改變的時候,全部依賴他的對象都會獲得通知。前端

回憶曾經

做爲一名前端開發人員,給DOM節點綁定事件但是再頻繁不過的事情。好比以下代碼vue

document.body.addEventListener('click',function () {
        alert(2333);
    },false);
    document.body.click();//模擬點擊事件
複製代碼

這裏咱們訂閱了document.body的click事件,當body被點擊的時候,他就向訂閱者發佈這個消息,彈出2333.咱們也能夠隨意的增長和刪除訂閱者,當消息一發布,全部的訂閱者都會收到消息。node

document.body.addEventListener('click',function () {
        alert(11111);
    },false);
    document.body.addEventListener('click',function () {
        alert(222);
    },false);
    document.body.addEventListener('click',function () {
        alert(333);
    },false);
    document.body.click();//模擬點擊事件
複製代碼

值得注意的是,手動觸發事件這裏咱們直接用了document.body.click();可是更好的作法是IE下用fireEvent,標準瀏覽器下用dispatchEvent,以下:瀏覽器

let fireEvent = function (element,event) {
        if (document.createEventObject) {
            var evt = document.createEventObject();
            return element.fireEvent('on'+event,evt);
        }else{
            var evt = document.createEvent('HTMLEvents');
            evt.initEvent(event,true,true);
            return element.dispatchEvent(evt);
        }
    }
    document.addEventListener('shout',function (event) {
        alert('shout');
    })
    fireEvent(document,'shout');
複製代碼

暢談如今

人的平常生活離不開各類人際交涉,好比你的朋友有不少,這時候你要結婚了,要以你爲發佈者,打開你的通信錄,挨個打電話通知各個訂閱者你要結婚的消息。抽象一下,實現發佈-訂閱模式須要:緩存

  1. 發佈者(你)
  2. 緩存列表(通信錄,你的朋友們至關於訂閱了你的全部消息)
  3. 發佈消息的時候遍歷緩存列表,依次觸發裏面存放的訂閱者的回調函數(挨個打電話)
  4. 另外,回調函數中還能夠添加不少參數,,訂閱者能夠接收這些參數,好比你會告訴他們婚禮時間,地點等,訂閱者收到消息後能夠進行各自的處理。
let yourMsg = {};
yourMsg.peopleList = [];
yourMsg.listen = function (fn) {
    this.peopleList.push(fn);
}
yourMsg.triger = function () {
    for(var i = 0,fn;fn=this.peopleList[i++];){
        fn.apply(this,arguments);
    }
}

yourMsg.listen(function (name) {
    console.log(`${name}收到了你的消息`);
})
yourMsg.listen(function (name) {
    console.log('哈哈');
})

yourMsg.triger('張三');
yourMsg.triger('李四');

複製代碼

  • 以上就是一個簡單的發佈-訂閱的實現,可是咱們會發現訂閱者會收到發佈者發佈的每一條信息,若是李四比較陰暗,不想聽到你結婚的消息,只想聽到你的壞消息,好比你被開除了,他就內心高興。這時候咱們就須要加一個key,讓訂閱者只訂閱本身感興趣的消息。
let yourMsg = {};
yourMsg.peopleList ={};
yourMsg.listen = function (key,fn) {
    if (!this.peopleList[key]) { //若是沒有訂閱過此類消息,建立一個緩存列表
        this.peopleList[key] = [];
    }
    this.peopleList[key].push(fn);
}
yourMsg.triger = function () {
    let key = Array.prototype.shift.call(arguments);
    let fns = this.peopleList[key];
    if (!fns || fns.length == 0) {//沒有訂閱 則返回
        return false;
    }
    for(var i=0,fn;fn=fns[i++];){
        fn.apply(this,arguments);
    }
}

yourMsg.listen('marrgie',function (name) {
    console.log(`${name}想知道你結婚`);
})
yourMsg.listen('unemployment',function (name) {
    console.log(`${name}想知道你失業`);
})

yourMsg.triger('marrgie','張三');
yourMsg.triger('unemployment','李四');
複製代碼

  • 你須要發佈消息,一樣的全部的人都有朋友圈,也都須要發佈消息,所以咱們有必要把發佈-訂閱的功能提取出來,放在一個單獨的對象內,誰須要誰去動態安裝發佈-訂閱功能(installEvent函數實現了動態安裝發佈-訂閱功能)。
var event = {
    peopleList:[],
    listen:function (key,fn) {
        if (!this.peopleList[key]) { //若是沒有訂閱過此類消息,建立一個緩存列表
        this.peopleList[key] = [];
        }
        this.peopleList[key].push(fn)
    },
    trigger:function () {
         let key = Array.prototype.shift.call(arguments);
        let fns = this.peopleList[key];
        if (!fns || fns.length == 0) {//沒有訂閱 則返回
            return false;
        }
        for(var i=0,fn;fn=fns[i++];){
            fn.apply(this,arguments);
        }
    }
}

var installEvent  = function (obj) {
    for(var i in event){
        obj[i] = event[i];
    }
}

let yourMsg = {};
installEvent(yourMsg);
yourMsg.listen('marrgie',function (name) {
    console.log(`${name}想知道你結婚`);
})
yourMsg.listen('unemployment',function (name) {
    console.log(`${name}想知道你失業`);
})

yourMsg.trigger('marrgie','張三');
yourMsg.trigger('unemployment','李四');
複製代碼
  • 有時間咱們須要取消訂閱的事件,好比李四是你的好朋友,可是由於一件事情,你倆鬧掰了,你把他從你的通信錄中給刪除掉了,這裏咱們給event增長一個remove方法;
remove:function (key,fn) {
      var fns = this.clientList[key];
      if(!fns){
          return false;
      }  
      if(!fn){
          fns && (fns.length=0)
      }else{
          for (let index = 0; index < fns.length; index++) {
              const _fn = fns[index];
              if(_fn === fn){
                  fns.splice(index,1);
              }
          }
      }
    }
複製代碼

發佈-訂閱的順序探討

咱們一般所看到的都是先訂閱再發布,可是必需要遵照這種順序嗎?答案是不必定的。若是發佈者先發布一條消息,可是此時尚未訂閱者訂閱此消息,咱們能夠不讓此消息消失於宇宙之中。就如同QQ離線消息同樣,離線的消息被保存在服務器中,接收人下次登陸以後,纔會收到此消息。一樣的,咱們能夠創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裝函數會被存入堆棧中,等到有對象來訂閱事件的時候,咱們將遍歷堆棧並依次執行這些包裝函數,即重發裏面的事件,不過離線事件的生命週期只有一次,就像qq未讀消息只會提示你一次同樣。bash

JavaScript實現發佈-訂閱模式的便利性

由於JavaScript有回調函數這個優點存在,咱們寫開發-訂閱顯得更簡單一點。傳統的發佈-訂閱好比Java一般會把訂閱者自身當成引用傳入發佈者對象中,同時訂閱者對象還需提供一個名爲諸如update的方法,供發佈者對象在合適的時候調用。下面代碼用js模擬下傳統的實現。服務器

function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function () {
    this.subs.forEach(sub=>sub.update());
}
function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function () {
     this.fn();
}

var dep = new Dep();
dep.addSub(new Watcher(function () {
    console.log('okokok');
}))
dep.notify();
複製代碼

小結

  • 發佈-訂閱的優點很明顯,作到了時間上的解耦和對象之間的解耦,從架構上看,MVC,MVVM都少不了發佈-訂閱的參與,咱們經常使用的Vue也是基於發佈-訂閱的,最近會抽時間寫下vue的源碼實現,一樣的node中的EventEmitter也是發佈訂閱的,以前也手寫過它的實現。
  • 發佈-訂閱同時也是有缺點存在的,建立訂閱者自己要消耗必定的時間和內存,並且當你訂閱一個消息之後,可能此消息最後都未發生,可是這個訂閱者會始終存在於內存中。若是程序中大量使用發佈-訂閱的話,也會使得程序跟蹤bug變得困難。
相關文章
相關標籤/搜索