由于这两天想看看怎么本身手写一个
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的实现,涉及到了这一块的知识,因此就找来看了看,以为还挺有意思,还能够这么写,因此就单独写篇博客记录一下。
我是前端战五渣,一个前端界的小学生。