发布-订阅设计模式对大家来说并不是很陌生,举一个最简单的例子,在前端开发过程中,事件的绑定就是其实际的应用。首先我们先了解下什么是发布-订阅模式。
基本概念:发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知。在前端开发中,我们一般用事件模型来替代传统的发布-订阅模式。
发布-订阅模式是前端常用的一种设计模式,现在主流的MVVM框架,都大量使用了此设计模式,其主要作用有以下两点:一是可以实现模块间通信,而是可以在一定程度上实现异步编程。其基本事件模型如下:
前端的事件绑定有三要素,一是传入事件类型,二是声明对应的回调方法,三是触发条件;触发条件为对应的事件类型。前端DOM的事件系统本质也是发布-订阅模式,而我们在业务处理中所应有的模式也与此类似,只不过发布订阅模式应用的是自定义事件类型,可以自定义。
发布订阅事件模型本质上就是一个中央事件处理总线,它接收所有订阅的自定义事件,并将自定义事件的回调存储到事件回调的堆栈中,当某在某个时刻触发了此自定义事件,会调用分发事件的方法,从事件回调堆栈中取出符合条件的事件回调,依次调用,具体实现逻辑如下:
class EventEmiter { constructor() { //回调中心 this.listenerList = {}; this.createEventList = {}; } /** * 添加事件监听 * @param {事件类型} type * @param {回调方法} fn */ on(type, fn) { if (!this.listenerList[type]) { this.listenerList[type] = []; } this.listenerList[type].push(fn); } /** * 触发事件监听 * @param {事件类型} type */ emit(type, flag) { let fnList = this.listenerList[type]; let params = Array.from(arguments); if (!fnList || fnList.length === 0) { return false; } else { fnList.forEach(fn => { fn.apply(this, params.slice(1)); }); } } /** * 移除事件监听 * @param {事件类型} type * @param {回调方法} fn */ off(type, fn) { if (!this.listenerList[type]) { return false; } else { let index = this.listenerList[type].findIndex(vv => vv === fn); if (index === -1) { return false; } else { this.listenerList[type].splice(index, 1); } } }}let eventBus = new EventEmiter();function cb(param) { console.log("this is a test", param);}eventBus.on("test", cb);eventBus.emit("test", 123);eventBus.off("test", cb);eventBus.emit("test", 456);
以上只是对发布订阅模式进行一个简单的实现,自定义事件只能分发给在触发前已订阅的消息,针对那些先触发,后订阅的内容,并不能得到一个很好的处理,所以,如果要解决这种弊端,就必须加一个离线的事件缓存。除此之外,发布订阅也有一些弊端,那就是每次发布消息,都会触发所有的事件监听回调,尽管大多数情况下并不想触发所有回调内容,所以在这种情况下,最好对事件加一些命名空间,以缩小其生效范围。
以下为支持离线事件代码,只是对事件加了一个标记:
class EventEmitter { constructor() { //回调中心 this.listenerMap = {}; //离线事件列表 this.offlineListenerList = []; } /** * 添加事件监听 * @param type 事件类型 * @param fn 回调函数 * @param flag 是否是离线事件 */ on(type, fn, flag) { if (!this.listenerMap[type]) { this.listenerMap[type] = []; } this.listenerMap[type].push(fn); //如果注册了离线事件,则在监听事件时,需要检测是否有离线事件缓存 if (flag) { let index = this.offlineListenerList.findIndex(vv => vv.type === type); if (index !== -1) { fn.call(this, this.offlineListenerList[index].params); //清空该条离线事件记录 this.offlineListenerList.splice(index, 1); } } } /** * 触发事件监听 * @param type 事件类型 * @param params 载荷参数 * @param flag */ emit(type, params, flag) { let fnList = this.listenerMap[type]; if (fnList && Array.isArray(fnList)) { fnList.forEach(fn => { fn.apply(this, params); }); } //如果注册的是离线事件,则吧 if (flag) { this.offlineListenerList.push({ type, params }); } } /** * 移除事件监听 */ off(type, fn) { if (!this.listenerMap[type]) { return false; } else { let index = this.listenerMap[type].findIndex(vv => vv === fn); if (index === -1) { return false; } else { this.listenerMap[type].splice(index, 1); } } } /** * 只触发一次 * @param type * @param fn */ once(type, fn) { let fnList = this.listenerMap[type]; let params = Array.from(arguments); if (!fnList || fnList.length === 0) { return false; } else { let index = fnList.findIndex( vv => vv === fn); fnList[index].apply(this, params.slice(1)); fnList.splice(index, 1); } }}let event = new EventEmitter();event.emit('test', 1, true);event.on('test', params => { console.log('offline', params)}, true);event.on('cc', () => { console.log('normal', 22222);});event.emit('cc');