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;