前端戰五渣學JavaScript——淺談發佈—訂閱模式

由於這兩天想看看怎麼本身手寫一個promise並實現基本功能,最一開始就看到了then方法須要涉及發佈-訂閱模式。。。因此有專門看了看到底什麼是發佈-訂閱模式javascript

咱們是如何綁定點擊監聽事件的

添加發布-訂閱

衆所周知啊,咱們在一個button或者任意一個標籤上綁定點擊事件的時候,都是先聲明瞭一個方法,而後再咱們點擊的時候纔會真正的去執行咱們綁定的這個方法,這就是一個比較典型的運用了發佈-訂閱模式的例子。html

咱們先來看代碼吧⬇️前端

index.htmljava

<button id="btn">點我</button>
複製代碼

index.jsreact

(() => {
  const btn = document.querySelector('#btn');
  btn.addEventListener('click', () => {
    console.log('第一個監聽事件');
  }, false);
  btn.addEventListener('click', () => {
    console.log('第二個監聽事件');
  }, false);
})();

複製代碼

咱們在一個按鈕上綁定了兩個點擊事件,這兩個事件都能接收到按鈕被點擊的信息,一個輸出「第一個監聽事件」,一個輸出「第二個監聽事件」
在咱們綁定事件的時候,咱們並不知道這兩個事件何時會被執行,反正綁上就完事了
這兩個事件並不會衝突,也不會覆蓋,而是在咱們點擊了按鈕之後會相繼執行redux

刪除訂閱

咱們既然能夠添加一個訂閱,那咱們確定在須要的時候也得能夠能刪除訂閱,因此原生api也是有這個功能,看代碼⬇️api

index.html數組

<button id="btn">點我</button>
  <button id="delete">刪除第二個訂閱事件</button>
複製代碼

index.jspromise

(() => {
  const btn = document.querySelector('#btn');
  const deleteBtn = document.querySelector('#delete');
  function firstFn() {
    console.log('第一個監聽事件');
  }
  function secondFn() {
    console.log('第二個監聽事件');
  }
  function deleteFirstFn() {
    btn.removeEventListener('click', secondFn); // 刪除btn上面的第二個監聽事件
  }
  btn.addEventListener('click', firstFn, false);
  btn.addEventListener('click', secondFn, false);
  deleteBtn.addEventListener('click', deleteFirstFn, false); // 點擊執行deleteFirstFn方法
})();
複製代碼

如今咱們頁面上是有兩個按鈕,當咱們點擊「點我」按鈕的時候,控制檯會輸出「第一個監聽事件」「第二個監聽事件」
咱們再點擊「刪除第二個訂閱事件」按鈕,這個時候咱們已經刪除了「點我」按鈕的第二個監聽事件
當咱們再次點擊「點我」按鈕的時候,發現控制檯只會輸出「第一個監聽事件」了,肯定咱們已經刪除了「點我」按鈕的第二個監聽事件
由於在移除監聽事件的時候,必需要指明須要刪除的監聽事件,因此咱們不能使用匿名函數緩存

咱們來用發佈-訂閱實現自定義事件吧

在使用發佈-訂閱模式寫咱們本身的事件以前,咱們先來設定一個背景故事

簡單的發佈-訂閱

我是來自真新鎮的小智,個人目標是成爲神奇寶貝大師,我從大木博士的研究所出發,一路收集我喜歡的神奇寶貝,當我捕捉到一個神奇寶貝的時候,我會把這個好消息告訴大木博士和個人媽媽。我出來好幾天了,我不想天天都給他們打電話告訴他們我有沒有捉到神奇寶貝,我只想在我捕捉到神奇寶貝的時候再通知他們,你能幫助我嗎??(越看越像小學應用題的語氣。。。。)

let littleZhi = {}; // 聲明一個小智
littleZhi.familyList = []; // 來一個緩存隊列,存放須要通知的親戚的回調函數
littleZhi.listen = function (fn) {
  this.familyList.push(fn); // 把須要通知的回調函數放起來
};
littleZhi.trigger = function () { // 通知家人
  for (let i = 0; i < this.familyList.length; i++ ) { // 當執行的時候,從familyList遍歷,挨個通知一遍
    let fn = this.familyList[i];
    fn.apply(this, arguments);
  }
};

littleZhi.listen(function(pokemon) { // 事先定好,若是我抓到了pokemon,我就告訴媽媽
  console.log(`媽媽,我抓到${pokemon}了!!!`)
});
littleZhi.listen(function(pokemon) { // 事先定好,若是我抓到了pokemon,我就告訴博士
  console.log(`大木博士,我抓到${pokemon}了!!!`)
});

littleZhi.trigger('綠毛蟲'); // 當抓到綠毛蟲的時候,通知媽媽和博士,由於他們兩個都跟小智說抓到了要告訴他們
littleZhi.trigger('比比鳥'); // 當抓到比比鳥的時候,通知媽媽和博士,由於他們兩個都跟小智說抓到了要告訴他們
複製代碼

這樣,咱們實現了當小智捉到神奇寶貝的時候,就會通知媽媽和大木博士。以前沒抓到的時候就不用通知了,無論在何時抓到了,只要執行littleZhi.trigger()這個方法,媽媽和大木博士就能夠收到通知了

可是咱們從輸出狀況也能看出來,當咱們抓到不論是綠毛蟲仍是比比鳥,媽媽和大木博士收到的消息是同樣的

添加標示,區分媽媽和博士

媽媽畢竟是媽媽,媽媽知道小智抓到了神奇寶貝,可是還想在知道抓到神奇寶貝的同時,還能瞭解一下小智的近況,因此咱們須要把監聽事件區分開來,咱們繼續來幫助他吧~

let littleZhi = {}; // 聲明一個小智
littleZhi.familyList = []; // 來一個緩存隊列,存放須要通知的親戚的回調函數
littleZhi.listen = function (key, fn) {
  if ( !this.familyList[key] ) {
    this.familyList[key] = []; // 相同key值的狀況下,若是尚未任何訂閱,就給該類消息建立一個緩存列表
  }
  this.familyList[key].push(fn); // 把須要通知的回調函數放起來
};
littleZhi.trigger = function () { // 通知家人
  let key = Array.prototype.shift.call(arguments); // 從傳入的參數裏面選取第一個,就是咱們傳入的key值特殊標示
  let fns = this.familyList[key]; // 取出在familyList中對應key值的事件隊列fns,再遍歷這個事件隊列挨個執行
  if (!fns || fns.length === 0) { // 若是傳入的key值沒有對應的事件隊列,或者有隊列,可是隊列是空的,就直接返回
    return false
  }
  for (let i = 0; i < fns.length; i++ ) { // 當執行的時候,從familyList遍歷,挨個通知一遍
    let fn = fns[i];
    fn.apply(this, arguments);
  }
};

littleZhi.listen('mama', function(pokemon, story) { // 事先定好,若是我抓到了pokemon,我就告訴媽媽,而後告訴她個人近況
  console.log(`媽媽,我抓到${pokemon}了!!!`);
  console.log(story);
});
littleZhi.listen('doctor', function(pokemon) { // 事先定好,若是我抓到了pokemon,我只告訴博士我抓到了什麼
  console.log(`大木博士,我抓到${pokemon}了!!!`);
});

littleZhi.trigger('mama', '綠毛蟲', '我還被皮卡丘電了'); // 通知媽媽我抓到綠毛蟲了,可是我被皮卡丘電了
littleZhi.trigger('doctor', '比比鳥'); // 通知博士我抓到了比比鳥
複製代碼

這樣咱們就區分開了給媽媽和博士不一樣的消息,咱們給媽媽消息的時候,這條消息就不會傳到大木博士那裏

誒??可是咱們(咱們的問題就是這麼多)若是出發的不僅是小智一我的,還有小茂呢??那小茂是否是也得從新寫一遍這些方法和隊列呢?

可根據不一樣人安裝事件

咱們都知道,跟小智一塊兒從真新鎮出發的還有小茂,那小茂出門在外固然也但願能往家裏傳遞他旅行的好消息,可是如今只有小智能夠通知家裏,因此咱們有什麼好辦法幫助小茂嗎??

const event = { // 咱們把須要的功能都單獨列出來,發佈,訂閱,隊列,以便後面須要的時候賦給須要的人
  familyList: [], // 來一個緩存隊列,存放須要通知的親戚的回調函數
  listen(key, fn) {
    if ( !this.familyList[key] ) {
      this.familyList[key] = []; // 相同key值的狀況下,若是尚未任何訂閱,就給該類消息建立一個緩存列表
    }
    this.familyList[key].push(fn); // 把須要通知的回調函數放起來
  },
  trigger() { // 通知家人
    let key = Array.prototype.shift.call(arguments); // 從傳入的參數裏面選取第一個,就是咱們傳入的key值特殊標示
    let fns = this.familyList[key]; // 取出在familyList中對應key值的事件隊列fns,再遍歷這個事件隊列挨個執行
    if (!fns || fns.length === 0) { // 若是傳入的key值沒有對應的事件隊列,或者有隊列,可是隊列是空的,就直接返回
      return false
    }
    for (let i = 0; i < fns.length; i++ ) { // 當執行的時候,從familyList遍歷,挨個通知一遍
      let fn = fns[i];
      fn.apply(this, arguments);
    }
  }
};

const installEvent = function(pokemonMaster) { // 在出發的神奇寶貝大師身上安裝發佈訂閱的功能
  for (let i in event) {
    pokemonMaster[i] = event[i];
  }
};

let littleZhi = {}; // 聲明小智
let littleMao = {}; // 聲明小茂

installEvent(littleZhi); // 給小智安裝能夠給家裏通知的技能
installEvent(littleMao); // 給小茂安裝能夠給家裏通知的技能

littleZhi.listen('littleZhiToDoctor', function(pokemon) { // 事先定好,若是小智抓到了pokemon,我只告訴博士我抓到了什麼
  console.log(`大木博士,我是小智,我抓到${pokemon}了!!!`);
});
littleMao.listen('littleMaoToDoctor', function(pokemon) { // 事先定好,若是小茂抓到了pokemon,我只告訴博士我抓到了什麼
  console.log(`大木博士,我是小茂,我抓到${pokemon}了!!!`);
});
// 小智的triger通知博士
littleZhi.trigger('littleZhiToDoctor', '比比鳥'); // 小智通知博士抓到了比比鳥 
// 小茂的triger通知博士
littleMao.trigger('littleMaoToDoctor', '小火龍'); // 小智通知博士抓到了小火龍


// 輸出:大木博士,我是小智,我抓到比比鳥了!!!
// 輸出:大木博士,我是小茂,我抓到小火龍了!!!
複製代碼

經過上面的改造,咱們就分別給小智littleZhi和小茂littleMao多賦予了發生事情能夠通知家裏的技能,因此不僅僅只有小智能夠了哦。

細心的小朋友可能會發現,其實不論是littleZhi仍是littleMao添加的listen,都存放在同一個familyList裏面,因此致使若是咱們在littleZhi.listen(key, fn)littleMao.listen(key, fn)傳若是相同的key,例如

littleZhi.listen('littleZhiToDoctor', function(pokemon) { // key爲littleZhiToDoctor
  console.log(`大木博士,我是小智,我抓到${pokemon}了!!!`);
});
littleMao.listen('littleZhiToDoctor', function(pokemon) { // key也爲littleZhiToDoctor
  console.log(`大木博士,我是小茂,我抓到${pokemon}了!!!`);
});
複製代碼

當咱們發佈消息的時候

littleZhi.trigger('littleZhiToDoctor', '比比鳥');
// 或者
littleMao.trigger('littleZhiToDoctor', '比比鳥');
複製代碼

不論是上面代碼執行哪一行,都會同時輸出「大木博士,我是小智,我抓到比比鳥了!!!」和「大木博士,我是小茂,我抓到比比鳥了!!!」和兩句話。。。。咱們能夠理解爲——電話串線了。。。。

由於咱們在installEvent()的時候,消息隊列是淺克隆(不懂深淺克隆的,能夠看個人另外一篇文章《前端戰五渣學JavaScript——深克隆(深拷貝)》),因此兩個被安裝了方法的對象中famalyList引用的是同一個數組,因此在收到發佈消息的時候會都執行。。。因此咱們在給對象賦予event對象的時候,須要判斷若是是familyList,須要採用深克隆的辦法。。。

因此咱們須要引入lodash的,用它裏面的深克隆方法。。。畢竟本身去實現深克隆很麻煩

const _ = require('lodash');
const event = {...};
const installEvent = function() { // 在出發的神奇寶貝大師身上安裝發佈訂閱的功能
  return _.cloneDeep(event);
};
let littleZhi = installEvent(); // 聲明小智
let littleMao = installEvent(); // 聲明小茂
...
複製代碼

這樣咱們即便在有相同key的listen的時候,各自的familyList裏面對應的key隊列也只有本身的函數。不會說小智trigger了一個key,小茂有,小茂也會執行的尷尬窘迫事情。

跟家裏鬧彆扭,不想通知了,刪除

刪除的功能通常用的不多吧。。。那咱們就來簡單的寫一下吧。

const event = {
  ...
  remove(key, fn) {
    let fns = this.familyList[key]; // 從事件隊列中拿到key值對應的事件數組
    if (!fns) { // 若是key值沒有對應的數組,就直接返回
      return false;
    }
    if (!fn) { // 若是沒有傳入fn,直接發key值對應的數組置空
      fns && ( fns.length = 0 )
    } else { // 反向遍歷事件數組,若是有跟傳入的函數是同一個的,就刪除掉
      for (let i = fns.length - 1; i >= 0; i-- ) {
        let _fn = fns[i];
        if (_fn === fn) {
          fns.splice(i, 1);
        }
      }
    }
  }
}
複製代碼

這樣咱們就完成了發佈訂閱模式的刪除功能。

Tips

這篇博客感受長度差很少了,可是還有幾點想說的,之後可能會單獨開博客講講吧

一個上面的發佈訂閱模式是簡陋的,只能完成特定事情的一個模型,可是基本的功能是能夠實現了的。再大型項目開發過程當中,咱們是能夠統一封裝一個Event對象來實現咱們上述的功能,以及定製化的功能。

還有一個是衆所周知,react項目中咱們能夠依賴redux來進行數據的統一管理,那這個redux其實也是運用到了發佈訂閱的模式,來實現不一樣模塊間的數據通訊。

最後其實還有相似發佈訂閱的最佳實踐尚未說到,好比一個組件中的事件執行以後,可能波及到好幾個組件進行各類處理,那咱們其餘的組件怎麼知道我這個組件發生了變化呢,那就是運用了發佈訂閱模式。之後單獨開一篇博客來說講吧

總結

其實咱們不是所用狀況都須要用到發佈訂閱模式的,發佈訂閱雖好,可不要貪杯哦~

可是這種模式有一些比較明顯的有點,就是時間上的解耦,咱們在定義好事件之後,咱們能夠在須要執行的時候去執行。

此篇博客原本不在計劃之中的,是想了解一下手寫promise的實現,涉及到了這一塊的知識,因此就找來看了看,以爲還挺有意思,還能夠這麼寫,因此就單獨寫篇博客記錄一下。


我是前端戰五渣,一個前端界的小學生。

相關文章
相關標籤/搜索