eventBus.js — 全局事件总线说明文档

eventBus.js --- 全局事件总线说明文档

概述

手写的发布-订阅模式实现,零外部依赖,用于 Vue 2 中非父子组件之间的轻量级通信。

核心是一个 EventBus 类,内部使用 Map 存储事件与回调数组的映射关系。


为什么需要 EventBus?

通信方式 适用场景 局限
props + $emit 父子组件 多层级时逐层传递繁琐
Vuex 全局状态管理 适合持久状态,轻量通知过重
provide / inject 祖孙组件 非响应式(Vue 2),调试不便
EventBus 跨组件事件通知 适合临时性、轻量级的通信

实现原理

复制代码
内部数据结构(Map):

  handlers = {
    "sidebar:toggle" → [fn1, fn2]
    "notify"         → [fn3]
  }

操作:
  $on("event", fn)      →  向数组中 push 回调
  $emit("event", data)  →  遍历数组,依次执行所有回调
  $off("event", fn)     →  从数组中移除指定回调
  $once("event", fn)    →  包装回调,执行一次后自动 $off

架构示意

复制代码
┌──────────┐                          ┌──────────┐
│  组件 A   │  $emit("sidebar", true)  │  组件 B   │
│ (发布者)  │ ──────────────────────→ │ (订阅者)  │
└──────────┘                          └──────────┘
                                          │
                                    $on("sidebar", fn)
                                    在 created 中注册
                                    在 beforeDestroy 中移除

组件 A 和组件 B 无需在组件树中存在父子关系,完全解耦。


API

$on(event, callback)

注册事件监听。同一事件可注册多个回调,触发时按注册顺序执行。

参数 类型 说明
event string 事件名称
callback Function 触发时执行的回调,接收 $emit 传递的参数

$once(event, callback)

$on,但触发一次后自动移除。

内部实现:包装原回调,执行后调用 $off 移除自身。包装函数上保留 _original 引用指向原回调,因此用过 $once 后仍可通过原函数引用 $off 主动移除。

$emit(event, ...args)

触发事件,将后续参数传递给所有已注册的回调。

$off(event, callback?)

移除监听:

传参 效果
只传 event 移除该事件下的所有回调
event + callback 只移除该特定回调

使用示例

基本用法

js 复制代码
import bus from "@/utils/eventBus";

// ===== 组件 A:发布事件 =====
export default {
  methods: {
    handleClick() {
      bus.$emit("sidebar:toggle", true);
      bus.$emit("notify", { type: "success", text: "操作成功" });
    },
  },
};

// ===== 组件 B:订阅事件 =====
export default {
  data() {
    return { collapsed: false };
  },
  created() {
    bus.$on("sidebar:toggle", this.onSidebarToggle);
  },
  beforeDestroy() {
    // 务必移除,防止内存泄漏
    bus.$off("sidebar:toggle", this.onSidebarToggle);
  },
  methods: {
    onSidebarToggle(collapsed) {
      this.collapsed = collapsed;
    },
  },
};

使用事件常量(推荐)

js 复制代码
import bus, { EVENTS } from "@/utils/eventBus";

// 发布
bus.$emit(EVENTS.SIDEBAR_TOGGLE, true);

// 订阅
bus.$on(EVENTS.SIDEBAR_TOGGLE, this.handleToggle);

使用 $once(一次性监听)

js 复制代码
bus.$once("app:ready", () => {
  console.log("应用初始化完成,仅执行一次");
});

批量移除某事件所有监听

js 复制代码
// 不传 callback,清空该事件下所有回调
bus.$off("sidebar:toggle");

注意事项

1. 必须在 beforeDestroy 中移除监听

js 复制代码
beforeDestroy() {
  bus.$off("some-event", this.handler);
}

否则组件被销毁后回调依然存在于 bus 上,造成:

  • 内存泄漏(回调持有组件实例引用)
  • 逻辑异常(已销毁组件的方法仍被调用)

2. 事件名称避免冲突

  • 推荐使用 : 分隔的命名空间格式:module:action
  • 使用 EVENTS 常量集中管理事件名

3. 不宜滥用

建议用 EventBus 建议用 Vuex
临时通知、跨组件触发行为 持久化状态、多处共享读取
"通知侧边栏收起" "当前用户信息"
"显示全局提示" "购物车列表"

4. $emit 中遍历的安全处理

$emit 内部对回调数组做了浅拷贝(.slice())后再遍历,因此即使回调中调用了 $off(如 $once 场景),也不会导致遍历过程出错。


项目预置事件常量

常量 事件名 用途
EVENTS.SIDEBAR_TOGGLE sidebar:toggle 侧边栏展开/收起
EVENTS.LOADING_CHANGE loading:change 全局 loading 状态
EVENTS.USER_UPDATED user:updated 用户信息更新
EVENTS.NOTIFY notify 全局消息通知

根据项目需求自行扩展。

javascript 复制代码
/**
 * @fileoverview 全局事件总线 (EventBus)
 *
 * 手写发布-订阅模式,零依赖,用于非父子组件之间通信。
 *
 * 适用场景:
 *   - 跨层级组件通信(兄弟组件、祖孙组件等)
 *   - 全局状态变更通知(侧边栏折叠、主题切换、消息提醒等)
 *   - 避免 prop 逐层传递和事件层层冒泡
 *
 * 使用方式:
 *   import bus from "@/utils/eventBus";
 *
 *   // 监听事件(通常在 created / mounted 中注册)
 *   bus.$on("event-name", handler);
 *
 *   // 触发事件
 *   bus.$emit("event-name", payload);
 *
 *   // 移除监听(通常在 beforeDestroy 中卸载,避免内存泄漏)
 *   bus.$off("event-name", handler);
 */

/**
 * 事件总线类
 *
 * 原理:
 *
 *   handlers = {
 *     "sidebar:toggle": [fn1, fn2],
 *     "notify":         [fn3],
 *   }
 *
 *   - $on     → 向数组中 push 回调
 *   - $emit   → 遍历数组依次执行回调
 *   - $off    → 从数组中移除指定回调
 *   - $once   → 包装回调,执行一次后自动调用 $off
 *
 * ┌──────────┐   $emit("xxx", data)   ┌──────────┐
 * │  组件 A   │ ────────────────────→ │  组件 B   │
 * │ (发布者)  │                       │ (订阅者)  │
 * └──────────┘                       └──────────┘
 *                                        $on("xxx", fn)
 */
class EventBus {
  constructor() {
    /**
     * 事件存储
     * key: 事件名(string)
     * value: 回调函数数组
     * @type {Map<string, Function[]>}
     */
    this._handlers = new Map();
  }

  /**
   * 注册事件监听
   *
   * @param {string} event - 事件名称
   * @param {Function} callback - 触发时执行的回调函数
   *
   * @example
   *   bus.$on("sidebar:toggle", (collapsed) => {
   *     this.collapsed = collapsed;
   *   });
   */
  $on(event, callback) {
    if (!this._handlers.has(event)) {
      this._handlers.set(event, []);
    }
    this._handlers.get(event).push(callback);
  }

  /**
   * 注册一次性事件监听(触发一次后自动移除)
   *
   * @param {string} event - 事件名称
   * @param {Function} callback - 触发时执行的回调函数
   *
   * @example
   *   bus.$once("app:ready", () => {
   *     console.log("应用初始化完成,仅执行一次");
   *   });
   */
  $once(event, callback) {
    // 创建一个包装函数,执行原回调后立即移除自身
    const wrapper = (...args) => {
      callback(...args);
      this.$off(event, wrapper);
    };
    // 保留原始 callback 引用,以便用户能通过原函数引用主动 $off
    wrapper._original = callback;
    this.$on(event, wrapper);
  }

  /**
   * 触发事件
   *
   * @param {string} event - 事件名称
   * @param {...*} args - 传递给回调的参数
   *
   * @example
   *   bus.$emit("notify", { type: "success", text: "保存成功" });
   *   bus.$emit("sidebar:toggle", true);
   */
  $emit(event, ...args) {
    const handlers = this._handlers.get(event);
    if (!handlers || handlers.length === 0) return;

    // 遍历执行时做一份浅拷贝,避免遍历过程中数组被修改(如 $once 中 $off 自身)
    handlers.slice().forEach((fn) => fn(...args));
  }

  /**
   * 移除事件监听
   *
   * - 传入 callback → 只移除该回调
   * - 不传 callback → 移除该事件下的所有回调
   *
   * @param {string} event - 事件名称
   * @param {Function} [callback] - 要移除的具体回调函数(可选)
   *
   * @example
   *   // 移除特定回调
   *   bus.$off("sidebar:toggle", this.onToggle);
   *
   *   // 移除该事件所有监听
   *   bus.$off("sidebar:toggle");
   */
  $off(event, callback) {
    if (!callback) {
      // 未传回调:清空该事件的所有监听
      this._handlers.delete(event);
      return;
    }

    const handlers = this._handlers.get(event);
    if (!handlers) return;

    // 过滤掉匹配的回调(含 $once 创建的包装函数)
    this._handlers.set(
      event,
      handlers.filter((fn) => fn !== callback && fn._original !== callback),
    );

    // 如果数组已空,删除该 key
    if (this._handlers.get(event).length === 0) {
      this._handlers.delete(event);
    }
  }
}

/**
 * 事件名称常量(建议集中管理,避免字符串散落各处)
 *
 * 使用方式:bus.$emit(EVENTS.SIDEBAR_TOGGLE, true)
 *
 * 按项目需求自行扩展
 */
export const EVENTS = {
  /** 侧边栏展开/收起 */
  SIDEBAR_TOGGLE: "sidebar:toggle",
  /** 全局 loading 状态变更 */
  LOADING_CHANGE: "loading:change",
  /** 用户信息更新 */
  USER_UPDATED: "user:updated",
  /** 消息通知 */
  NOTIFY: "notify",
};

/** 全局事件总线单例 */
const bus = new EventBus();

export default bus;
相关推荐
小玮看世界1 小时前
[Python]从“脏”数据到优雅实现:一个IoT滑动窗口最大值问题的测试驱动优化实录
linux·前端·python
尤乐娃子2 小时前
进入大厂(厂子大)实习Day11
前端·笔记·实习
xiaoxiangsiyan2 小时前
运维之前端反调试学习
运维·前端·学习·状态模式
小徐_23332 小时前
TRAE WORK 实战,之前写一篇水文要半天,现在用 TRAE Work 摸鱼2分钟交差,真香!
前端·trae
明月_清风2 小时前
🤗 Hugging Face 模型上传完全指南:从本地到 Hub 的 4 种姿势
前端·后端·ai编程
sugar__salt2 小时前
跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
前端·javascript·vue.js·前端框架·vue
To_OC3 小时前
从一个名字编辑组件开始,我把 React + TS 的数据流和副作用彻底搞明白了
前端·react.js·typescript
程序员黑豆3 小时前
Java入门第一步:从零开始编写你的第一个Hello World程序
java·前端·ai编程
凌涘3 小时前
前端路由(二):React Router 组件化路由实战
前端