- 所有的设计模式都是用来有效管理代码的
- 便捷开发
- 通俗易懂
- 有助于后期代码维护和升级
- 发布订阅设计模式(观察者模式的升级版)
- 发布一个计划,并且向计划中订阅一个个的方法
- 当触发某个事件或者到达了某个阶段,我们可以通知计划中订阅的方法按照顺序依次执行
方案一:不支持自定义事件,且页面只有一个事件池(基于单例设计模式)
let sub = (function(){
//创建自定义事件池
let pond = [];
//订阅、移除订阅、通知执行
const on = function on(func){
//去重处理
for(let i = 0; i < pond.length;i++){
if(pond[i] === func){
return;
}
}
pond.push(func);
}
const off = function off(func){
for(let i = 0; i < pond.length;i++){
if(pond[i] === func){
//这样会导致数组塌陷
//pond.splice(i, 1);
//先把要移除的项置为null
pond[i] = null;
break;
}
}
}
const fire = function fire(...params){
for(let i = 0; i < pond.length;i++){
let itemFunc = pond[i];
//因为数组中有null值了,所以这里执行前需进行判断,null则不再执行
if(typeof itemFunc !== 'function'){
//同时在这里将数组中null值移除
pond.splice(i, 1);
i--;//防止数组塌陷
continue;
}
itemFunc(...params);
}
}
return{
on,
off,
fire
}
})();
//==========================测试, 1秒后执行fn1~fn5方法=======
const fn1 = ()=>{console.log('fn1')}
const fn2 = ()=>{console.log('fn2 '); sub.off(fn1);}
const fn3 = ()=>{console.log('fn3')}
const fn4 = ()=>{console.log('fn4')}
const fn5 = ()=>{console.log('fn5')}
sub.on(fn1);
sub.on(fn2);
sub.on(fn3);
sub.on(fn4);
sub.on(fn5);
setTimeout(()=>{
//传统方法需要把fn1~fn5放这里分别调用
//利用发布订阅模式,则直接调用触发事件方法即可
sub.fire();
}, 1000);
方案二:支持自定义事件
在上面的方案中,因为整个页面只有一个事件池,那么不管触发什么事件(例如:body的click或box的click事件)都会执行同样的代码,也就是说都会把事件池中的事件,全都执行一次。那么有时候想点body时执行fn1和fn2;点box时执行fn3~fn5,那么上一种方案就显然无法满足了。接下来我们将上面的方案改造一下,让其支持自定义事件。
let sub = (function(){
//创建自定义事件池
let pond = {};
//订阅、移除订阅、通知执行
const on = function on(event, func){
//如果pond中没有这个数组,默认添加一个
!pond.hasOwnProperty(event) ? pond[event] = [] : null;
let arr = pond[event];
!arr.includes(func) ? arr.push(func) : null;
}
const off = function off(event, func){
let arr = pond[event];
//如果arr是undefined,则直接return
if(!arr) return;
for(let i = 0; i < arr.length;i++){
if(arr[i] === func){
//这样会导致数组塌陷
//arr.splice(i, 1);
//先把要移除的项置为null
arr[i] = null;
break;
}
}
}
const fire = function fire(event, ...params){
let arr = pond[event];
//如果arr是undefined,则直接return
if(!arr) return;
for(let i = 0; i < arr.length;i++){
let itemFunc = arr[i];
//因为数组中有null值了,所以这里执行前需进行判断,null则不再执行
if(typeof itemFunc !== 'function'){
//同时在这里将数组中null值移除
arr.splice(i, 1);
i--;//防止数组塌陷
continue;
}
itemFunc(...params);
}
}
return{
on,
off,
fire
}
})();
//==========================测试, 1秒后执行fn1~fn5方法=======
const fn1 = ()=>{console.log('fn1')}
const fn2 = ()=>{console.log('fn2 ');sub.off('BODY-CLICK',fn1)}
const fn3 = ()=>{console.log('fn3')}
const fn4 = ()=>{console.log('fn4')}
const fn5 = ()=>{console.log('fn5')}
sub.on('BODY-CLICK',fn1);
sub.on('BODY-CLICK',fn2);
sub.on('BOX',fn3);
sub.on('BOX',fn4);
sub.on('BOX',fn5);
dcument.body.onclick = function(){
sub.fire('BODY-CLICK');
}
setTimeout(()=>{
//传统方法需要把fn1~fn5放这里分别调用
//利用发布订阅模式,则直接调用触发事件方法即可
sub.fire('BOX');
}, 1000);
方案三:创建多个事件池,每个事件池是独立的存放自己的订阅方法,但也可以互相共用(基于面向对象中的类和实例)
(function(){
class Sub{
constructor(){
//私有属性
this.pond = [];
}
//原型上的公共方法
on(func){
!this.pond.includes(func) ? this.pond.push(func) : null;
}
off(func){
this.pond.forEach((item, index)=>{
item === func ? pond[index] = null : null;
});
}
fire(...params){
for(let i = 0; i < this.pond.length;i++){
let itemFunc = this.pond[i];
//因为数组中有null值了,所以这里执行前需进行判断,null则不再执行
if(typeof itemFunc !== 'function'){
//同时在这里将数组中null值移除
this.pond.splice(i, 1);
i--;//防止数组塌陷
continue;
}
itemFunc(...params);
}
}
}
// window.sub = ()=> new Sub();
window.sub = function(){
return new Sub();
}
})();
//==========================测试, 1秒后执行fn1~fn5方法=======
const fn1 = ()=>{console.log('fn1')}
const fn2 = ()=>{console.log('fn2 '); sub2.off(fn3);}
const fn3 = ()=>{console.log('fn3')}
const fn4 = ()=>{console.log('fn4')}
const fn5 = ()=>{console.log('fn5')}
let sub1 = sub(),
sub2 = sub();
sub1.on(fn1);
sub1.on(fn2);
sub2.on(fn3);
sub2.on(fn4);
sub2.on(fn5);
setTimeout(()=>{
//传统方法需要把fn1~fn5放这里分别调用
//利用发布订阅模式,则直接调用触发事件方法即可
sub1.fire();
}, 1000);
setTimeout(()=>{
//传统方法需要把fn1~fn5放这里分别调用
//利用发布订阅模式,则直接调用触发事件方法即可
sub1.fire();
}, 2000);