web前端高级JavaScript - 基于多种方案实现发布订阅设计模式 | 8月更文挑战

240 阅读3分钟
  • 所有的设计模式都是用来有效管理代码的
    • 便捷开发
    • 通俗易懂
    • 有助于后期代码维护和升级
  • 发布订阅设计模式(观察者模式的升级版)
    • 发布一个计划,并且向计划中订阅一个个的方法
    • 当触发某个事件或者到达了某个阶段,我们可以通知计划中订阅的方法按照顺序依次执行 在这里插入图片描述

方案一:不支持自定义事件,且页面只有一个事件池(基于单例设计模式)

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);