1 什么是发布订阅模式
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。 其实发布订阅模式在前端轮子中经常用到, 只是作为使用者我们感知不强罢了, 比如常用的window.addEventListner。 发布订阅模式在前端中的妙用可以参考下面这篇文章【发布订阅模式的巧妙应用 - 掘金 (juejin.cn)】
2 发布订阅模式优点
- 松耦合
- 易维护
- 解决负载问题
3 js实现发布订阅模式
建议看官不要着急写代码, 先把本文通读一遍再靠着自己的理解去写。
- 首先我们需要实现一个调度中心,我们暂且命名为eventChannel
- 有一个注册事件的函数,我们可以叫他on,listener,subscribe, 我们暂定为on函数
- 有一个取消注册的函数, 我们暂定为off函数
- 发布者有一个发布函数,可以向事件发布数据,我们叫他emit函数
那么这时候问题就来了, 发布者发布数据后, 我们怎么通知订阅者呢? 答案就是回调函数! 我们在事件中心, 以事件名为key,回调函数组成的数组为value, 这样当发布者发布消息的时候, 就可以根据事件名去调度中心找到对应的订阅者的回调函数, 然后再调用回调函数即可。
3.1 实现事件调度中心和注册事件的函数
// 事件调度中心
const eventChannel = {}
/**
* on, 是订阅者的监听函数
* @param {*} eventName 事件名称,
* @param {*} callback 回调函数
*/
const on = (eventName, callback) => {
// 因为发布订阅模式可以有多个
if (eventChannel[eventName] === undefined) {
eventChannel[eventName] = []
}
eventChannel[eventName].push(callback)
}
因为订阅者可能有多个, 所以回调函数放到数组中
3.2 实现取消订阅的函数(off)
/**
* 取消订阅
* @param {*} eventName
* @param {*} callback 如经过不传回调函数,则清空事件对应的所有回调函数
*/
const off = (eventName, callback) => {
if (!callback) {
eventChannel[eventName] = []
} else {
// 根据事件名称,删除数组中的callback
let idx = eventChannel[eventName].indexOf(callback);
if (idx > -1) {
eventChannel[eventName].splice(idx, 1);
}
}
}
3.3 实现发布函数(emit)
/**
* emit是发布者使用的发布函数
* @param {*} eventName
* @param {...any} args
*/
const emit = (eventName, ...args) => {
const events = eventChannel[eventName];
if (Array.isArray(events)) {
events.forEach(event => event(...args))
}
}
3.4 实现once函数
通过回调函数和临时函数变量, 实现once函数
/**
* 执行单次事件订阅, 触发后自动取消订阅
* @param {*} eventName 事件名称
* @param {*} callback 回调函数
*/
const once = (eventName, callback) => {
const tempCallback = (args) => {
this.off(eventName, args);
callback(args);
}
this.on(eventName, tempCallback);
}
3.5 全部代码如下
// 事件调度中心
const eventChannel = {}
/**
* on, 是订阅者的监听函数
* @param {*} eventName 事件名称,
* @param {*} callback 回调函数
*/
const on = (eventName, callback) => {
// 因为发布订阅模式可以有多个
if (eventChannel[eventName] === undefined) {
eventChannel[eventName] = []
}
eventChannel[eventName].push(callback)
}
/**
* emit是发布者使用的发布函数
* @param {*} eventName
* @param {...any} args
*/
const emit = (eventName, ...args) => {
const events = eventChannel[eventName];
if (Array.isArray(events)) {
events.forEach(event => event(...args))
}
}
/**
* 取消订阅
* @param {*} eventName
* @param {*} callback 如经过不传回调函数,则清空事件对应的所有回调函数
*/
const off = (eventName, callback) => {
if (!callback) {
eventChannel[eventName] = []
} else {
// 根据事件名称,删除数组中的callback
let idx = eventChannel[eventName].indexOf(callback);
if (idx > -1) {
eventChannel[eventName].splice(idx, 1);
}
}
}
/**
* 执行单次事件订阅, 触发后自动取消订阅
* @param {*} eventName 事件名称
* @param {*} callback 回调函数
*/
const once = (eventName, callback) => {
const tempCallback = (args) => {
this.off(eventName, args);
callback(args);
}
this.on(eventName, tempCallback);
}
module.exports = {
on,
emit,
off,
once
}
4 测试代码以及测试结果
const eventBus = require('./001PubSub')
eventBus.on('午饭', (data) => {
console.log(`小明${data}该吃午饭了`)
})
eventBus.on('午饭', (data) => {
console.log(`小红过了${data}午饭了`)
})
eventBus.emit('午饭', '11点')