Skip to content

学习笔记:前端发布订阅模式 (Event Bus)

本文稿的核心是讲解前端开发者必须掌握的设计模式之一——发布订阅模式,并通过从零开始实现一个 EventBus 来深入理解其原理和应用。

1. 核心概念与应用场景

  • 是什么? 发布订阅模式是一种消息范式,其中发布者(Publishers)不会将消息直接发送给特定的订阅者(Subscribers),而是通过一个称为“事件总线”或“消息中心”的第三方组件进行通信。发布者发布事件,订阅者订阅事件,两者之间相互解耦。

  • 为什么重要? 它是前端面试的高频考点,也是解决复杂组件通信问题的利器。

  • 应用场景: 在 Vue.js (或其他前端框架) 中,用于兄弟组件两个无直接关联的组件之间的传值和通信。

    • 示例: HelloWorld 组件(发布者)中有一个按钮,点击该按钮会触发一个事件。Child 组件(订阅者)监听这个事件,并根据接收到的数据更新自身的 count 状态。

2. 从零实现一个 EventBus

我们将创建一个 EventBus 类,它包含订阅、发布、取消订阅和一次性订阅的核心功能。

2.1. 基础结构
typescript
class EventBus {
    // 用于存储事件和对应回调函数的容器
    private events: { [key: string]: Set<Function> };

    constructor() {
        this.events = {};
    }

    // ... methods
}
  • events 属性:一个对象(字典),键是事件名称 (string),值是一个 Set 集合,用于存储所有订阅了该事件的回调函数。
  • 为什么用 Set 而不是数组?
    • 自动去重Set 数据结构不允许重复值。如果同一个回调函数对同一个事件多次订阅,Set 会自动保证只存储一次,避免了不必要的重复执行。
    • 删除方便:使用 Set.delete() 方法可以快速移除指定回调,而数组则需要通过 filtersplice 等相对复杂的方式来操作。
2.2. on(eventName, callback) - 订阅事件

此方法用于将一个回调函数 callback 订阅到指定的 eventName 上。

typescript
on(eventName: string, callback: Function) {
    // 如果事件名不存在,则使用空值合并运算符(??)初始化一个新的 Set
    this.events[eventName] = this.events[eventName] ?? new Set();
    this.events[eventName].add(callback);
}
  • 逻辑解析
    1. 检查 this.events 中是否存在 eventName 对应的 Set
    2. 如果不存在(即为 nullundefined),则创建一个新的 Set 并赋值给它。
    3. callback 添加到对应的 Set 中。
2.3. emit(eventName, ...args) - 发布事件

当事件发生时,调用此方法来执行所有已订阅的回调函数,并可以传递参数。

typescript
emit(eventName: string, ...args: any[]) {
    const callbacks = this.events[eventName];
    if (!callbacks) {
        return; // 如果没有订阅者,则直接返回
    }

    callbacks.forEach(callback => {
        callback(...args);
    });
}
  • 逻辑解析
    1. 根据 eventName 找到对应的 Set
    2. 如果存在,则遍历 Set 中的所有 callback
    3. 使用剩余参数 (...args) 将 emit 接收到的所有参数原封不动地传递给每个 callback 并执行。
2.4. off(eventName, callback) - 取消订阅

用于移除对特定事件的某个指定回调函数的监听。

typescript
off(eventName: string, callback: Function) {
    const callbacks = this.events[eventName];
    if (callbacks) {
        callbacks.delete(callback);
    }
}
  • 逻辑解析
    1. 找到 eventName 对应的 Set
    2. 如果 Set 存在,直接调用其 delete 方法移除特定的 callback。这比数组操作更高效。
2.5. once(eventName, callback) - 一次性订阅

订阅一个事件,但对应的回调函数在执行一次后会自动被移除。

typescript
once(eventName: string, callback: Function) {
    // 创建一个临时的包装函数
    const wrapper = (...args: any[]) => {
        // 先执行原始的回调
        callback(...args);
        // 然后立即将自身从订阅中移除
        this.off(eventName, wrapper);
    };

    // 使用 on 方法订阅这个包装函数
    this.on(eventName, wrapper);
}
  • 逻辑解析(核心技巧)
    1. 不直接订阅原始的 callback
    2. 创建一个新的 wrapper 函数,这个函数是真正被订阅的。
    3. 当事件被 emit 时,wrapper 函数被执行。
    4. wrapper 函数内部首先调用原始的 callback,完成其任务。
    5. 紧接着,调用 this.off() 方法,将 wrapper 函数自身从事件监听中移除。
    6. 这样,下一次同样的事件被 emit 时,wrapper 已不存在,从而实现“只执行一次”的效果。
    7. 注意:在原始文稿中,提到了 this 指向问题,如果 wrapper 是一个普通 function,其内部的 this 可能不是 EventBus 实例。使用箭头函数可以完美解决这个问题,因为它会捕获定义时所在的上下文的 this

3. 实践应用(Vue 示例)

步骤 1:创建并导出单例

通常 EventBus 在整个应用中是唯一的(单例模式),以确保所有组件共享同一个事件中心。

typescript
// eventBus.ts
class EventBus {
  // ... 上述所有代码
}

// 导出的是类的实例,而不是类本身
export default new EventBus();
步骤 2:订阅方 (Child 组件)
vue
<script setup>
import { ref, onUnmounted, watchEffect } from 'vue';
import bus from './eventBus';

const count = ref(0);

// 定义处理函数
const incrementHandler = (value) => {
  count.value += value;
};

// 订阅事件 (once 示例)
bus.once('increment', incrementHandler); // 只会触发一次,count 增加到 2 后失效

/*
// 订阅事件 (on 示例)
bus.on('increment', incrementHandler);

// 在组件销毁时,最好取消订阅,防止内存泄漏
onUnmounted(() => {
  bus.off('increment', incrementHandler);
});

// 条件性取消订阅 (off 示例)
watchEffect(() => {
  if (count.value >= 10) {
    console.log('Count is >= 10, removing listener.');
    bus.off('increment', incrementHandler);
  }
});
*/
</script>
步骤 3:发布方 (HelloWorld 组件)
vue
<template>
  <button @click="handleClick">Increment Child's Count</button>
</template>

<script setup>
import bus from './eventBus';

const handleClick = () => {
  // 发布事件,并传递参数 2
  bus.emit('increment', 2);
};
</script>

4. 总结

发布订阅模式通过一个中央 EventBus 解耦了组件之间的直接依赖,使得非父子组件间的通信变得简单、可维护。虽然在日常业务开发中可能更多地使用框架自带的状态管理工具(如 Pinia/Vuex),但理解并能手写一个 EventBus 是衡量前端工程师基础是否扎实的重要标准,尤其在面试中。