由於這兩天想看看怎麼本身手寫一個
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);
}
}
}
}
}
複製代碼
這樣咱們就完成了發佈訂閱模式的刪除功能。
這篇博客感受長度差很少了,可是還有幾點想說的,之後可能會單獨開博客講講吧
一個上面的發佈訂閱模式是簡陋的,只能完成特定事情的一個模型,可是基本的功能是能夠實現了的。再大型項目開發過程當中,咱們是能夠統一封裝一個Event
對象來實現咱們上述的功能,以及定製化的功能。
還有一個是衆所周知,react項目中咱們能夠依賴redux來進行數據的統一管理,那這個redux其實也是運用到了發佈訂閱的模式,來實現不一樣模塊間的數據通訊。
最後其實還有相似發佈訂閱的最佳實踐尚未說到,好比一個組件中的事件執行以後,可能波及到好幾個組件進行各類處理,那咱們其餘的組件怎麼知道我這個組件發生了變化呢,那就是運用了發佈訂閱模式。之後單獨開一篇博客來說講吧
其實咱們不是所用狀況都須要用到發佈訂閱模式的,發佈訂閱雖好,可不要貪杯哦~
可是這種模式有一些比較明顯的有點,就是時間上的解耦,咱們在定義好事件之後,咱們能夠在須要執行的時候去執行。
此篇博客原本不在計劃之中的,是想了解一下手寫promise的實現,涉及到了這一塊的知識,因此就找來看了看,以爲還挺有意思,還能夠這麼寫,因此就單獨寫篇博客記錄一下。
我是前端戰五渣,一個前端界的小學生。