JavaScript展示实现简单事件总线的代码示例,分析事件通信机制

大白话JavaScript展示实现简单事件总线的代码示例,分析事件通信机制

前端小伙伴们,有没有被组件通信逼到挠头?父子组件用props传值还能忍,可跨页面、跨模块的组件通信,难道要一路props钻到底?或者为了简单通信去引入Redux,学半天发现“杀鸡用牛刀”?今天咱们就聊一个轻量又好用的方案——事件总线(Event Bus),用JavaScript手写一个,从此组件通信so easy~

一、组件通信的"三大痛点"

先说说我上周踩的坑:做一个电商页面,需要实现“购物车添加商品→顶部通知栏显示成功”的功能。结果:

  • 跨组件通信难:购物车组件和通知栏没父子关系,props传值得经过中间组件“中转”,代码乱成一团;
  • 全局状态太“重”:为了这么个小功能引入Redux,得写actionreducerstore,光配置就花半小时;
  • 耦合度高:组件间直接引用,改个通知栏样式,购物车组件也得跟着调整,维护起来头大。

这些痛点总结起来就一句话:轻量级跨组件通信需要更简单、更解耦的方案,而事件总线就是来解决这个问题的!

二、事件总线的"发布-订阅模式"

事件总线的核心是发布-订阅模式(Pub/Sub),就像小区的“广播系统”:

  • 订阅者(Subscriber):在广播系统登记“我想听天气预报”;
  • 发布者(Publisher):广播员喊“明天晴天”;
  • 事件总线(Event Bus):广播系统负责记录谁想听什么,发布时通知所有登记的人。

1. 三大核心角色:

  • 事件中心:存储事件与回调的映射(比如{ 'add-cart': [fn1, fn2] });
  • 订阅(on):注册事件回调(告诉广播系统“我想听”);
  • 发布(emit):触发事件(广播员开始广播);
  • 取消订阅(off):移除事件回调(不想听了,取消登记)。

三、代码示例:从0到1实现事件总线

示例1:基础版事件总线(满足90%需求)

先实现一个最基础的事件总线,包含on(订阅)、emit(发布)、off(取消订阅)三个核心方法。

// 事件总线类(ES6语法)
class EventBus {
  constructor() {
    // 存储事件与回调的映射:{ 事件名: [回调函数1, 回调函数2] }
    this.events = Object.create(null); 
  }

  /**
   * 订阅事件
   * @param {string} eventName 事件名(如:'add-cart')
   * @param {function} callback 回调函数(事件触发时执行)
   * @returns {function} 取消订阅的函数(方便直接调用)
   */
  on(eventName, callback) {
    // 如果事件不存在,初始化空数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 将回调添加到事件数组中
    this.events[eventName].push(callback);
    // 返回取消订阅的函数(闭包保存eventName和callback)
    return () => this.off(eventName, callback);
  }

  /**
   * 发布事件(触发事件)
   * @param {string} eventName 事件名
   * @param {...any} args 传递给回调的参数(如:商品信息)
   */
  emit(eventName, ...args) {
    // 如果事件不存在,直接返回
    if (!this.events[eventName]) return;
    // 遍历事件数组,依次执行回调(用slice复制数组,避免回调中off导致的数组长度变化问题)
    this.events[eventName].slice().forEach(callback => {
      callback(...args);
    });
  }

  /**
   * 取消订阅
   * @param {string} eventName 事件名
   * @param {function} callback 要移除的回调函数
   */
  off(eventName, callback) {
    // 如果事件不存在,直接返回
    if (!this.events[eventName]) return;
    // 过滤掉要移除的回调(注意:严格匹配引用)
    this.events[eventName] = this.events[eventName].filter(
      cb => cb !== callback
    );
    // 如果事件数组为空,删除该事件键(可选优化)
    if (this.events[eventName].length === 0) {
      delete this.events[eventName];
    }
  }
}

// 创建事件总线实例(全局可用)
const eventBus = new EventBus();

示例2:进阶版——支持一次性订阅(once)

有时候我们希望事件只触发一次(如“用户登录成功后只提示一次”),可以添加once方法:

class EventBus {
  // ...(保留之前的on/emit/off方法)

  /**
   * 一次性订阅(事件触发后自动取消订阅)
   * @param {string} eventName 事件名
   * @param {function} callback 回调函数
   */
  once(eventName, callback) {
    // 包装原回调:执行后自动off
    const wrapper = (...args) => {
      callback(...args); // 执行原回调
      this.off(eventName, wrapper); // 执行后取消自己
    };
    // 订阅包装后的回调
    this.on(eventName, wrapper);
  }
}

示例3:实战——购物车与通知栏通信

用事件总线实现“添加商品到购物车→通知栏显示成功”的功能:

// ------------------ 购物车组件 ------------------
// 添加商品到购物车的函数
function addToCart(product) {
  // 模拟添加逻辑(如调用API)
  console.log(`添加商品:${product.name}`);
  // 发布事件:通知其他组件商品已添加
  eventBus.emit('cart-added', product);
}

// ------------------ 通知栏组件 ------------------
// 订阅cart-added事件,显示通知
const unsubscribe = eventBus.on('cart-added', (product) => {
  showNotification(`已添加 ${product.name} 到购物车`);
});

// 如果需要,在组件卸载时取消订阅(避免内存泄漏)
// unsubscribe(); 

// 辅助函数:显示通知(模拟)
function showNotification(text) {
  console.log(`通知:${text}`);
  // 实际项目中可能是DOM操作或调用UI库组件
}

// ------------------ 测试 ------------------
// 模拟用户点击添加商品
addToCart({ name: 'iPhone 15', price: 6999 });
// 输出:
// 添加商品:iPhone 15
// 通知:已添加 iPhone 15 到购物车

示例4:处理匿名回调的坑(用Symbol标识)

如果订阅时用了匿名函数,直接off会失效(因为匿名函数引用不同)。可以用Symbol作为唯一标识:

// 用Symbol生成唯一事件名(避免命名冲突)
const CART_ADDED = Symbol('cart-added');

// 订阅时用命名函数或Symbol标识
eventBus.on(CART_ADDED, function handleCartAdded(product) {
  showNotification(`已添加 ${product.name}`);
});

// 取消订阅时,传递相同的函数引用
eventBus.off(CART_ADDED, handleCartAdded);

四、事件总线 vs 其他通信方式

用表格对比不同方案的优缺点,帮你选最适合的通信方式:

对比项事件总线(Event Bus)Props传值Redux/Vuex自定义事件(DOM)
通信复杂度低(全局事件)高(需层级传递)中(需store/action)低(依赖DOM)
解耦程度高(组件无直接依赖)低(强依赖层级)高(通过store解耦)中(依赖DOM结构)
适用场景轻量级跨组件通信父子组件通信复杂状态管理同页面DOM元素通信
学习成本低(只需掌握on/emit/off)低(React/Vue基础)高(需学习redux-saga等)低(需了解DOM事件)
内存泄漏风险中(需手动取消订阅)低(无额外订阅)低(store自动管理)中(需移除事件监听)

五、面试题回答方法

正常回答(结构化):

“事件总线基于发布-订阅模式实现,核心机制是:

  1. 事件中心:用对象存储事件名与回调函数数组的映射;
  2. 订阅(on):将回调函数添加到对应事件的数组中;
  3. 发布(emit):遍历事件数组,执行所有回调并传递参数;
  4. 取消订阅(off):从事件数组中移除指定回调,避免内存泄漏;
  5. 一次性订阅(once):通过包装回调函数,触发后自动取消订阅。
    适用场景包括轻量级跨组件通信、模块解耦等,需注意及时取消订阅以避免内存泄漏。”

大白话回答(接地气):

“事件总线就像小区的广播系统——

  • 你想收快递(订阅事件),就去物业登记(on);
  • 快递到了(发布事件),物业广播通知所有登记的人(emit);
  • 搬家了(组件卸载),得去物业取消登记(off),不然快递还会往旧地址送(内存泄漏)。
    好处是大家不用互相留电话(解耦),坏处是得自己记着取消登记(手动管理)。”

六、总结:3个核心方法+2个避坑指南

3个核心方法:

  • on(eventName, callback):注册事件回调;
  • emit(eventName, ...args):触发事件并传递参数;
  • off(eventName, callback):取消事件回调(重要!避免内存泄漏)。

2个避坑指南:

  • 避免匿名回调:订阅时尽量用命名函数或Symbol标识,否则无法正确off
  • 及时取消订阅:组件卸载时务必取消订阅(如在useEffect的清理函数中调用off),否则回调会一直存在,导致内存泄漏;
  • 事件命名规范:用Symbol或前缀(如'cart.add')避免事件名冲突(尤其是大型项目)。

七、扩展思考:4个高频问题解答

问题1:事件总线会导致全局污染吗?

解答
事件总线本身是一个全局实例(如eventBus),但只要合理管理事件名和回调,不会导致全局污染。可以通过:

  • 模块化:按功能划分不同的事件总线实例(如cartEventBususerEventBus);
  • 命名空间:用'module.event'格式命名事件(如'cart.add''user.login');
  • 封装成工具库:将事件总线封装为独立模块,避免直接暴露全局变量。

问题2:如何调试事件总线?

解答

  • 添加日志:在on/emit/off方法中添加console.log,输出事件名、参数、回调数量;
  • 使用DevTools:用window.eventBus暴露实例,在浏览器控制台手动触发事件测试;
  • 封装调试工具:添加listEvents方法,打印所有事件及回调数量:
    class EventBus {
      // ...
      listEvents() {
        return Object.keys(this.events).map(eventName => ({
          eventName,
          callbackCount: this.events[eventName].length
        }));
      }
    }
    

问题3:事件总线和Vue的 o n / on/ on/emit有什么区别?

解答
Vue的$on/$emit是事件总线的一种实现,本质相同,但:

  • 作用域:Vue的事件总线绑定在Vue实例上(如this.$on),而自定义事件总线是独立的全局对象;
  • 生命周期:Vue的事件会随组件卸载自动清理(需在beforeUnmount$off),自定义事件总线需手动管理;
  • 生态集成:Vue的事件总线与组件生命周期深度集成,自定义事件总线更通用(可用于任何JavaScript环境)。

问题4:事件总线适合大型项目吗?

解答
事件总线适合轻量级通信,但大型项目需注意:

  • 复杂度控制:过多事件会导致“事件地狱”(满屏幕emit,难以追踪数据流);
  • 替代方案:复杂状态管理建议用Redux/Pinia(可追踪、可预测);
  • 结合使用:事件总线可作为补充,处理Redux不擅长的“即时通信”(如通知、提示)。

结尾:事件总线——轻量级通信的"瑞士军刀"

事件总线是前端开发中非常实用的工具,尤其适合轻量级跨组件通信。它简单、灵活,能快速解决props钻取和全局状态库“过重”的问题。记住:合理使用事件总线,及时清理回调,它会成为你开发中的好帮手~

下次遇到跨组件通信的问题,别忘了试试事件总线!如果这篇文章帮你理清了思路,记得点个赞,咱们下期,不见不散!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布洛芬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值