面试官:请你动手实现一个简单的发布-订阅模式

在JavaScript开发中,订阅发布模式是一种常见且强大的设计模式,它允许对象之间进行松散耦合的通信,从而提高了代码的可维护性和可扩展性。本文将深入探讨订阅发布模式的原理、用途以及如何在JavaScript中实现。

什么是订阅发布模式?

订阅发布(Publish-Subscribe)模式是一种软件架构模式。在这种模式中,有两种角色:

  1. 发布者(Publisher):负责发布事件或消息。
  2. 订阅者(Subscriber):订阅了发布者发布的事件或消息,并在事件发生时做出响应。

订阅发布模式的基本思想是解耦发布者和订阅者之间的关系。发布者不需要直接知道订阅者的存在,订阅者也不需要直接知道发布者的存在。它们通过一个中介(通常是一个事件总线或事件管理器)进行通信。

我们通过一个图辅助理解:

在订阅发布模式中,发布者将事件发送到事件总线或事件管理器中,然后订阅者订阅感兴趣的事件。当事件发生时,事件总线或事件管理器会通知所有订阅了该事件的订阅者,并调用它们预先注册的回调函数,从而实现了一种松耦合的通信方式。

为什么使用订阅发布模式?

订阅发布模式提供了一种松散耦合的通信方式,让对象之间的关联更加灵活。它的主要优点包括:

  1. 解耦性(Decoupling): 发布者和订阅者之间是松散耦合的,彼此不需要直接知道对方的存在。这意味着它们可以独立地进行修改、扩展和维护,而不会影响彼此。

  2. 灵活性(Flexibility): 订阅发布模式允许系统中的不同部分在不影响彼此的情况下进行通信。发布者不需要知道订阅者的具体实现,也不需要关心订阅者的数量或身份。

  3. 扩展性(Scalability): 通过订阅发布模式,可以轻松地添加新的订阅者或发布者,而无需修改现有的代码。这使得系统能够更容易地适应新的需求和功能。

订阅发布模式的实现

在JavaScript中,我们可以使用以下方式来实现订阅发布模式:

  1. 自定义实现: 我们可以手动创建一个管理事件的对象,并在其中实现订阅、发布和取消订阅的功能。这种方式需要我们管理事件列表、订阅者列表等,相对繁琐。
  2. 使用现有库: 许多JavaScript库和框架已经提供了订阅发布模式的实现,例如Node.js的EventEmitter模块、jQuery的事件系统等。通过使用这些库,我们可以更加方便地实现订阅发布功能。

实现:JavaScript中的订阅发布模式

示例一:

下面是一个简单的JavaScript实现一个简单的事件触发和订阅机制

javascript 复制代码
class EventEmitter {
    constructor() { 
    // 初始化事件和回调函数的存储对象
        this.handles = {}; 
    }
    
    // 订阅事件
    on(eventName, callback) { 
        if (!this.handles[eventName]) { 
            this.handles[eventName] = []; 
        }
        this.handles[eventName].push(callback); 
    }
    
    // 触发事件
    emit(eventName) { 
        if (this.handles[eventName]) { 
        // 获取事件对应的回调函数数组
            const handles = this.handles[eventName]; 
            handles.forEach(callback => { 
                callback(); 
            });
        }
    }
}

// 创建一个事件管理器实例
const emitter = new EventEmitter(); 

// 订阅'onSell'事件,添加两个回调函数
emitter.on('onSell', () => {
    console.log('hello'); 
});
emitter.on('onSell', () => {
    console.log('world'); 
});

emitter.emit('onSell'); // 触发'onSell'事件

上面的代码具体来说:

  1. EventEmitter 类用于管理事件的订阅和触发。
  2. on(eventName, callback) 方法用于订阅事件。当事件被触发时,对应的回调函数会被调用。
  3. emit(eventName) 方法用于触发事件。当事件被触发时,所有订阅该事件的回调函数都会被依次调用。

在代码中的例子中,订阅了 onSell 事件,并添加了两个回调函数。当 onSell 事件被触发时,这两个回调函数都会被执行,分别输出信息 "hello" "world"

实例二

在示例一的基础上新增一个取消订阅的功能:

javascript 复制代码
class EventEmitter {
    constructor() {
        this.handles = {}
    }

    // 订阅事件,将回调函数添加到指定事件的回调数组中
    on(eventName, cb) {
        if (!this.handles[eventName]) {
            this.handles[eventName] = []
        }
        this.handles[eventName].push(cb)
    }

    // 触发指定事件,依次调用对应事件的所有回调函数
    emit(eventName) {
        if (this.handles[eventName]) {
            const handles = this.handles[eventName]
            handles.forEach(cb => {
                cb()
            });
        }
    }

    // 取消订阅指定事件的指定回调函数
    off(eventName, cb) {
        const handles = this.handles[eventName]
        const index = handles && handles.indexOf(cb)
        if (index !== -1) {
            handles.splice(index, 1)
        }
    }
}

const emiter = new EventEmitter()

function wan() {
    console.log('hello');
}

function wu() {
    console.log('world');
}

// 订阅事件
emiter.on('onSell', wan)
emiter.on('onSell', wu)

// 取消订阅指定事件的指定回调函数
emiter.off('onSell', wan)

// 触发指定事件,执行对应的回调函数
emiter.emit('onSell')

在这段代码中,首先订阅了两个回调函数 wanwu 到事件 'onSell' 上。然后取消了回调函数 wan 的订阅。最后,触发了事件 'onSell'

因此,最终的输出将是:

复制代码
world

因为在触发事件 'onSell' 时,只有回调函数 wu 仍然订阅着,所以只会执行回调函数 wu

这段代码实现了基本的事件订阅、发布和取消订阅功能:

  1. on(eventName, cb): 这个方法用于订阅指定事件的回调函数。如果之前没有订阅过该事件,会在 this.handles 中创建一个数组用于存储回调函数,然后将回调函数 cb 添加到该数组中。

  2. emit(eventName): 这个方法用于触发指定事件,会按照订阅的顺序依次调用对应事件的回调函数。

  3. off(eventName, cb): 这个方法用于取消订阅指定事件的指定回调函数。它会找到对应事件的回调函数数组,然后删除数组中的指定回调函数 cb

实例三

根据上面两段实例我们接下来实现仅订阅一次事件

kotlin 复制代码
class EventEmitter {
    constructor() {
        this.handles = {} // 用于存储事件名和对应的回调函数数组
    }

    // 订阅事件,将回调函数添加到指定事件的回调数组中
    on(eventName, cb) {
        if (!this.handles[eventName]) {
            this.handles[eventName] = [] 
        }
        this.handles[eventName].push(cb) 
    }

    // 触发指定事件,依次调用对应事件的所有回调函数
    emit(eventName) {
        if (this.handles[eventName]) {
            const handles = this.handles[eventName]
            handles.forEach(cb => {
                cb() // 依次调用事件的回调函数
            });
        }
    }

    // once 方法用于仅订阅一次事件,即回调函数只会被执行一次
    once(eventName, cb) {
        // 如果事件已经存在,并且该回调函数已经被订阅过,则直接返回,不再重复订阅
        if (this.handles[eventName] && this.handles[eventName].indexOf(cb) !== -1) {
            return
        }
        // 否则,调用 on 方法订阅事件
        this.on(eventName, cb)
    }

}

const emiter = new EventEmitter()

function wu() {
    console.log('hello world');
}

// 使用 once 方法订阅事件,回调函数只会被执行一次
emiter.once('onSell', wu)
emiter.once('onSell', wu)
emiter.once('onSell', wu)

emiter.emit('onSell') // 触发事件,只会执行一次 wu() 回调函数

尽管我们上面多次使用 once 方法订阅事件,但是回调函数只会执行一次,所以输出一次 hello world

通过上述方法,用户可以灵活地管理事件和对应的回调函数,实现事件的订阅和触发。once 方法尤其适用于只需要执行一次回调函数的场景,避免了重复订阅导致多次执行的问题。

结论

订阅发布模式是一种非常有用的设计模式,它提供了一种松散耦合的通信方式,可以帮助我们构建更加灵活和可扩展的应用程序。通过深入理解订阅发布模式的原理和实现方式,我们可以更好地利用它来提高代码的质量和可维护性。

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom9 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github