手摸手用JS实现一个简单的发布-订阅模式

203 阅读3分钟

1 什么是发布订阅模式

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。 其实发布订阅模式在前端轮子中经常用到, 只是作为使用者我们感知不强罢了, 比如常用的window.addEventListner。 发布订阅模式在前端中的妙用可以参考下面这篇文章【发布订阅模式的巧妙应用 - 掘金 (juejin.cn)

image.png

2 发布订阅模式优点

  • 松耦合
  • 易维护
  • 解决负载问题

image.png

3 js实现发布订阅模式

建议看官不要着急写代码, 先把本文通读一遍再靠着自己的理解去写。

  1. 首先我们需要实现一个调度中心,我们暂且命名为eventChannel
  2. 有一个注册事件的函数,我们可以叫他on,listener,subscribe, 我们暂定为on函数
  3. 有一个取消注册的函数, 我们暂定为off函数
  4. 发布者有一个发布函数,可以向事件发布数据,我们叫他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点')

image.png