开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
1、什么是发布订阅模式
发布订阅模式主要由发布者、订阅者、消息通道三部分组成,多个订阅者可以同时监听一个发布者,当发布者状态发生变化时,就会通过消息通道将新的状态广播给各订阅者。
一个实际生活中的小🌰子:
小明和同学都在掘金上关注了某一场公开课,当公开课即将开始的某段时间内,运营(假设)会编辑消息推送到各个同学的账号,提醒同学去观看。
在这个过程中,小明和同学们就是订阅者,运营就是发布者,而掘金平台就扮演了消息通道的角色。
2、前端实现发布订阅模式
-
定义
消息通道(消息队列) -
定义
订阅者监听发布者方法 -
定义
发布者通知订阅者方法 -
定义
订阅者取消监听发布者方法
class EventEmitter {
constructor() {
this.events = Object.create(null);
}
/**
* 事件监听
* @param {String} event 事件名
* @param {Function} fn 回调函数
*/
on(event, fn) {
(this.events[event] || (this.events[event] = [])).push(fn);
return this;
}
/**
* 事件发布
* @param {String} event 事件名
*/
emit(event, ...args) {
let cbs = this.events[event];
if(cbs) {
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(this, args);
}
}
}
/** 取消事件监听
* @param {String} event 事件名
* @param {Function} fn 回调函数名
*/
off(event, fn) {
// this.off() 清空所有监听函数
if(!arguments.length) {
this.events = Object.create(null);
}
// this.off(event) 清空该事件的监听函数
if(!fn) {
this.events[event] = null;
}
let cbs = this.events[event]
if(!cbs) {
return this;
}
let cb;
let i = cbs.length;
while(i --) {
cb = cbs[i]
// XXX: 待补充,当使用once监听事件时,此处判断需要改为 (cb === fn || cb.fn === fn)
if(cb === fn) {
cbs.splice(i, 1);
break;
}
}
return this;
}
}
至此,一个基本的发布订阅模式就实现了,具体用法如下:
// 定义回调函数,供后续使用
const f1 = function(a) {
console.log('关注的公开课f1马上就要开始啦!', a);
}
const f2 = function(a) {
console.log('关注的公开课f2马上就要开始啦!', a);
}
const f3 = function(a, b) {
console.log('关注的公开课f3马上就要开始啦!');
console.log(a + b);
}
// 创建一个Event实例
const e = new EventEmitter();
// 注册监听事件(订阅者)
e.on('follow', f1);
e.on('follow', f2);
e.on('follow', f3);
// 发布事件(发布者):
// 所有监听了 follow 事件的订阅者都会执行对应的回调函数,即上述的 f1 f2 f3。3,7会作为回调函数的形参传入
e.emit('follow', 3, 7); // ==》 关注的公开课f1马上就要开始啦!3,关注的公开课f2马上就要开始啦!3,关注的公开课f3马上就要开始啦!10
// 注册监听事件 jym,回调函数:f1,f2
e.on('jym', f1);
e.on('jym', f2);
// 发布事件 jym,f1、f2执行
e.emit('jym');
// 取消监听事件 jym的回调函数f1
e.off('jym', f1);
// 只执行 f2
e.emit('jym');
- 扩展:定义once方法,监听事件发布后失效,即只执行一次回调函数
/** 取消事件监听
* @param {String} event 事件名
* @param {Function} fn 回调函数名
*/
off(event, fn) {
// this.off() 清空所有监听函数
if(!arguments.length) {
this.events = Object.create(null);
}
// this.off(event) 清空该事件的监听函数
if(!fn) {
this.events[event] = null;
}
let cbs = this.events[event]
if(!cbs) {
return this;
}
let cb;
let i = cbs.length;
while(i --) {
cb = cbs[i]
// 如果注册的监听事件是once,则原回调函数存在cb.fn中
if(cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
return this;
}
/**
* 监听事件,只执行一次回调函数
* @param {String} event 事件名
* @param {Function} fn 回调函数
*/
once(event, fn) {
// 最终注册监听事件的回调函数是该方法
function on(...args) {
// 利用off方法取消监听事件
this.off(event, on);
fn.apply(this, args);
}
// 缓存原回调函数
on.fn = fn;
// 利用this.on方法监听事件
this.on(event, on);
return this;
}
测试代码:
e.once('jym', f1);
e.emit('jym');
e.emit('jym');
// 结果:只执行了一次f1方法
总结
平时工作中很难用到这些经典的设计模式,最近看Vue2源码中用到了该模式实现eventbus,就找了一些发布订阅模式相关的文章学习,在此记录一下。