任务58: 单例模式和发布订阅模式
1.定义
所谓发布订阅就相当于订一个计划表,把我们到达条件干的事情先一步步列在计划表里面。等真到达条件的时候,我们就通知计划表中的方法一个个去执行。比如,先列一个结婚的计划表:把结婚当天要做的事情一件件都列举出来。结婚时:按照计划表中的计划一件件的去执行即可。
2.实现原理
(1)发布订阅(按照了DOM2事件池的机制)
①创建事件池
②向事件池中追加方法/移除方法
③通知事件池中的方法执行
(2)两种数据结构代码实现方式
第一种用数组,数组就是容器,每往进加一个事件,就创建一个对象,对象中由几个标识,一个数组容器当中,每加一条数据都是一个对象,对象中有类型,也有它要执行的函数。
第二种对象是一个容器,对象里是事件池的容器,我要往这个事件上加方法,相当于对象中每个数个就是自定义事件。项目中用的是这种结构,这种结构以后处理的时候会简单一点。
3.源码实现
本节目的是实现一个公共的库,公共的插件实现发布订阅。在真实项目中我们一般都用面向对象进行插件组件类库的封装,这样既能保证我们每次调用的时候都能创建一个不同的实例,而实例当中再进行…。里面提供的属性既有私有方法也有公有的属性,规划来说相对来说比较好一点,而且也符合我们JS本身的底层面向对象编程思想。
【知识小点】
用includes也能是否判断引用类型,且是通过地址来判断的,只要地址相同,引用地址是一样的就可以。
(function () {
const hasOwn = Object.prototype.hasOwnProperty;
class Sub {
//创建事件池
pond = {};//es7写法<=>constructor(){this.pond = {};}
//SUB.PROTOTYPE
on(type, func) {
let pond = this.pond,//把this.pond的堆内存地址给了pond。
listeners;
!hasOwn.call(pond, type) ? pond[type] = [] : null;//如果还没有这个事件,就在pond中加入这个事件,值等于空数组,如果有了什么都不做。
listeners = pond[type];//把pond[type]堆内存地址给listeners,以后往listeners加东西相当于往ponde[type]加。
!listeners.includes(func) ? listeners.push(func) : null;//如果listenrs里没有就往里面添加方法,如果有就什么都不做。
}
off(type, func) {
let pond = this.pond,//拿到事件池
listeners = pond[type] || [];//拿到事件池中某一自定义事件对应的容器,没有的话是空数组
if (listeners.length === 0) return;//如果是空数组就不用移除了
for (let i = 0; i < listeners.length; i++) {
if (listeners[i] === func) {
listeners.splice(i, 1);
return;
}
}
}
fire(type, ...params) {
let pond = this.pond,//拿到事件池
listeners = pond[type] || [];//拿到事件池中某一自定义事件对应的容器,没有的话是空数组
if (listeners.length === 0) return;
for (let i = 0; i < listeners.length; i++) {
let itemFunc = listeners[i];
itemFunc(...params);
}
}
}
window.subscribe = function subscribe() {
return new Sub();
};
})();
检验1:
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
}
function fn3() {
console.log(3);
}
function fn4() {
console.log(4);
}
function fn5(n, m) {
console.log(5, n, m);
}
let plan = subscribe();
plan.on('A', fn1);
plan.on('A', fn2);
plan.on('A', fn3);
plan.on('A', fn4);
plan.on('A', fn5);
setTimeout(() => {
plan.fire('A', 10, 20);
}, 2000)
检验2:
由检验1看,上面代码看似没什么问题,现在我们想实现一个需求。在fn2中移除两个前两个方法,使点击页面后第一输出12345 10 20,第二次输出3 4 5 10 20。
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
plan.off('A',fn1);
plan.off('A',fn2)
}
function fn3() {
console.log(3);
}
function fn4() {
console.log(4);
}
function fn5(n, m) {
console.log(5, n, m);
}
let plan = subscribe();
plan.on('A', fn1);
plan.on('A', fn2);
plan.on('A', fn3);
plan.on('A', fn4);
plan.on('A', fn5);
document.onclick = () => {
plan.fire('A', 10, 20)
}
然而实际的点击结果确实如下,与预期不符:
什么原因?这是因为源码中listeners.splice(i, 1)会导致数组塌陷,即当前计划表中方法按照顺序执行其中某个方法把计划表中的某些项删除掉,这样数组会場陷,可能导致当前通知执行的操作会跳过某些项去循环处理。
解决方法:第一次当数组方法重的时候,先用null占位,第二次去除null的时候,先减减,后加加以保证因索引变化导致的跳过函数。
(function () {
const hasOwn = Object.prototype.hasOwnProperty;
class Sub {
//创建事件池
pond = {};//es7写法<=>constructor(){this.pond = {};}
//SUB.PROTOTYPE
on(type, func) {
let pond = this.pond,//把this.pond的堆内存地址给了pond。
listeners;
!hasOwn.call(pond, type) ? pond[type] = [] : null;//如果还没有这个事件,就在pond中加入这个事件,值等于空数组,如果有了什么都不做。
listeners = pond[type];//把pond[type]堆内存地址给listeners,以后往listeners加东西相当于往ponde[type]加。
!listeners.includes(func) ? listeners.push(func) : null;//如果listenrs里没有就往里面添加方法,如果有就什么都不做。
}
off(type, func) {
let pond = this.pond,//拿到事件池
listeners = pond[type] || [];//拿到事件池中某一自定义事件对应的容器,没有的话是空数组
if (listeners.length === 0) return;//如果是空数组就不用移除了
for (let i = 0; i < listeners.length; i++) {
if (listeners[i] === func) {
// listeners.splice(i, 1);会导致数组塌陷
listeners[i] = null;
return;
}
}
}
fire(type, ...params) {
let pond = this.pond,//拿到事件池
listeners = pond[type] || [];//拿到事件池中某一自定义事件对应的容器,没有的话是空数组
if (listeners.length === 0) return;
for (let i = 0; i < listeners.length; i++) {
let itemFunc = listeners[i];
```
if(typeof itemFunc !=="function"){
listeners.splice(i,1);
i--;
continue;
}
itemFunc(...params);
```
}
}
}
window.subscribe = function subscribe() {
return new Sub();
};
})();
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
plan.off('A', fn1);
plan.off('A', fn2)
}
function fn3() {
console.log(3);
}
function fn4() {
console.log(4);
}
function fn5(n, m) {
console.log(5, n, m);
}
let plan = subscribe();
plan.on('A', fn1);
plan.on('A', fn2);
plan.on('A', fn3);
plan.on('A', fn4);
plan.on('A', fn5);
document.onclick = () => {
plan.fire('A', 10, 20)
}