Vue核心特性04,非父子组件通信:事件总线(Bus)的实现与使用场景

2025博客之星年度评选已开启 10w+人浏览 1.9k人参与

在前端开发中,组件化是核心思想之一。Vue、React 等框架的组件化让代码复用性和可维护性大幅提升,但组件间的通信问题也随之而来。父子组件通信有明确的解决方案(如 Vue 的 props/emit、React 的 props / 回调),但非父子组件(兄弟组件、跨层级组件)的通信,一度成为开发者的小痛点。

事件总线(Event Bus,简称 Bus)就是解决这一问题的经典方案之一。它轻量、灵活,无需复杂的状态管理库,就能实现非父子组件间的消息传递。本文将详细讲解事件总线的实现原理、在主流框架中的具体用法,以及它的适用场景与局限性。

一、事件总线的核心原理

事件总线的本质是发布 - 订阅模式(Publish/Subscribe Pattern),也叫观察者模式。它的核心思想是:

  1. 定义一个全局的 “事件中心”(Bus),这个中心具备事件订阅(on)、事件发布(emit)、事件取消订阅(off)的能力。
  2. 组件 A 可以向事件中心订阅某个事件,并绑定处理函数。
  3. 组件 B 可以向事件中心发布该事件,并传递数据。
  4. 事件中心收到发布的事件后,触发所有订阅该事件的处理函数,实现组件间的数据传递。

简单来说,事件总线就像一个 “消息中转站”,组件之间不直接通信,而是通过这个中转站传递消息,从而解耦组件间的依赖关系。

二、事件总线的基础实现(原生 JavaScript)

在了解框架中的用法前,我们先通过原生 JavaScript 实现一个简易的事件总线,理解其底层逻辑:

class EventBus {
  constructor() {
    // 存储事件与对应的回调函数集合,结构:{ eventName: [callback1, callback2] }
    this.events = {};
  }

  /**
   * 订阅事件
   * @param {string} eventName 事件名称
   * @param {Function} callback 回调函数
   */
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  /**
   * 发布事件(触发事件)
   * @param {string} eventName 事件名称
   * @param {any} data 传递的数据
   */
  emit(eventName, data) {
    const callbacks = this.events[eventName];
    if (callbacks && callbacks.length) {
      callbacks.forEach(callback => {
        callback(data);
      });
    }
  }

  /**
   * 取消订阅事件
   * @param {string} eventName 事件名称
   * @param {Function} callback 要取消的回调函数(不传则取消该事件的所有订阅)
   */
  off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (!callbacks) return;

    // 若不传 callback,清空该事件的所有订阅
    if (!callback) {
      this.events[eventName] = [];
      return;
    }

    // 过滤掉要取消的回调函数
    this.events[eventName] = callbacks.filter(cb => cb !== callback);
  }
}

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

// 测试:组件 A 订阅事件
bus.on('message', (data) => {
  console.log('组件 A 收到消息:', data); // 组件 A 收到消息:Hello Bus
});

// 测试:组件 B 发布事件
bus.emit('message', 'Hello Bus');

// 取消订阅
// const callback = (data) => console.log('组件 A 收到消息:', data);
// bus.on('message', callback);
// bus.off('message', callback);

这个简易的 EventBus 类包含了核心的 onemitoff 方法,足以支撑基础的事件发布与订阅。接下来我们看看在主流框架中如何使用事件总线。

三、Vue 中事件总线的实现与使用

Vue 本身提供了事件系统($on$emit$off),因此在 Vue 中实现事件总线非常便捷,主要有两种方式:

方式一:Vue 实例作为 Bus(Vue 2.x)

  1. 创建全局 Bus在项目的入口文件(如 main.js)中创建一个 Vue 实例,并挂载到 Vue 原型上:

    import Vue from 'vue';
    // 创建事件总线实例
    const bus = new Vue();
    // 挂载到 Vue 原型,所有组件可通过 this.$bus 访问
    Vue.prototype.$bus = bus;
    
    new Vue({
      el: '#app',
      render: h => h(App)
    });
    
  2. 组件中订阅事件假设我们有一个兄弟组件 ComponentA,需要订阅 refresh-data 事件:

    <template>
      <div>组件 A</div>
    </template>
    
    <script>
    export default {
      mounted() {
        // 订阅 refresh-data 事件,绑定处理函数
        this.$bus.$on('refresh-data', (data) => {
          console.log('收到数据:', data);
          // 执行数据刷新逻辑
        });
      },
      beforeDestroy() {
        // 组件销毁前取消订阅,防止内存泄漏
        this.$bus.$off('refresh-data');
      }
    };
    </script>
    
  3. 组件中发布事件另一个兄弟组件 ComponentB 需要发布 refresh-data 事件,并传递数据:

    <template>
      <button @click="sendData">发送数据</button>
    </template>
    
    <script>
    export default {
      methods: {
        sendData() {
          // 发布 refresh-data 事件,传递数据
          this.$bus.$emit('refresh-data', { name: 'Vue Bus', age: 18 });
        }
      }
    };
    </script>
    

方式二:使用 mitt 库(Vue 3.x)

Vue 3.x 中移除了 $on$off 等实例方法,因此无法直接用 Vue 实例作为 Bus。此时可以使用轻量的事件库 mitt(体积仅 200B 左右):

  1. 安装 mitt

    npm install mitt --save
    
  2. 创建全局 Bus在 src/utils/bus.js 中创建并导出 mitt 实例:

    import mitt from 'mitt';
    // 创建 mitt 实例
    const bus = mitt();
    export default bus;
    
  3. 组件中使用

    <!-- ComponentA.vue:订阅事件 -->
    <template>
      <div>组件 A</div>
    </template>
    
    <script setup>
    import { onMounted, onUnmounted } from 'vue';
    import bus from '@/utils/bus';
    
    onMounted(() => {
      // 订阅事件
      bus.on('refresh-data', (data) => {
        console.log('收到数据:', data);
      });
    });
    
    onUnmounted(() => {
      // 取消订阅
      bus.off('refresh-data');
    });
    </script>
    
    <!-- ComponentB.vue:发布事件 -->
    <template>
      <button @click="sendData">发送数据</button>
    </template>
    
    <script setup>
    import bus from '@/utils/bus';
    
    const sendData = () => {
      // 发布事件
      bus.emit('refresh-data', { name: 'Mitt Bus', age: 18 });
    };
    </script>
    

四、React 中事件总线的实现与使用

React 本身没有内置的事件系统,但我们可以使用前面写的原生 JavaScript EventBus 类,或直接使用 mitt 库。以下是使用 mitt 的示例:

  1. 安装 mitt

    npm install mitt --save
    
  2. 创建全局 Bus

    // src/utils/bus.js
    import mitt from 'mitt';
    export const bus = mitt();
    
  3. 组件中使用

    // ComponentA.jsx:订阅事件
    import { useEffect } from 'react';
    import { bus } from './utils/bus';
    
    const ComponentA = () => {
      useEffect(() => {
        // 订阅事件
        const handleRefresh = (data) => {
          console.log('收到数据:', data);
        };
    
        bus.on('refresh-data', handleRefresh);
    
        // 组件卸载时取消订阅
        return () => {
          bus.off('refresh-data', handleRefresh);
        };
      }, []);
    
      return <div>组件 A</div>;
    };
    
    export default ComponentA;
    
    // ComponentB.jsx:发布事件
    import { bus } from './utils/bus';
    
    const ComponentB = () => {
      const sendData = () => {
        // 发布事件
        bus.emit('refresh-data', { name: 'React Bus', age: 18 });
      };
    
      return <button onClick={sendData}>发送数据</button>;
    };
    
    export default ComponentB;
    

五、事件总线的使用场景

事件总线的轻量性和灵活性,使其适用于以下场景:

1. 小型项目 / 简单场景的非父子组件通信

对于小型项目,或仅需要少量非父子组件通信的场景(如兄弟组件间的简单数据传递、跨层级组件的状态通知),使用事件总线无需引入 Vuex、Redux 等复杂的状态管理库,能快速解决问题,降低项目复杂度。

2. 临时的组件通信需求

在开发过程中,可能会遇到临时的组件通信需求(如某个弹窗关闭后通知列表组件刷新),此时使用事件总线可以快速实现,无需重构现有代码结构。

3. 跨组件的事件通知

当需要实现跨组件的事件通知(如全局的消息提示、加载状态变更),事件总线可以作为轻量的通知机制,让多个组件响应同一个事件。

六、事件总线的局限性与注意事项

虽然事件总线很便捷,但它也存在一些局限性,使用时需要注意:

1. 缺乏统一的事件管理,易造成维护困难

随着项目规模扩大,事件名称可能会泛滥,且事件的发布和订阅分散在各个组件中,难以追踪事件的流向,后期维护成本会升高。

2. 容易导致内存泄漏

如果组件订阅了事件,但在销毁时没有取消订阅(off),会导致回调函数一直存在于内存中,长期运行可能引发内存泄漏。因此,组件卸载 / 销毁时必须取消订阅

3. 无法处理复杂的状态管理

对于需要共享复杂状态、支持状态回溯、多组件依赖同一状态的场景,事件总线无法满足需求,此时应使用 Vuex、Pinia、Redux 等专门的状态管理库。

4. 事件名称冲突

如果多个组件使用相同的事件名称,可能会导致意外的事件触发。建议使用命名空间的方式命名事件(如 user/refreshorder/change),避免冲突。

七、总结

事件总线(Bus)是基于发布 - 订阅模式实现的轻量通信方案,它完美解决了非父子组件间的简单通信问题,在小型项目、临时需求、跨组件事件通知等场景中表现出色。

但我们也要清楚它的局限性:在中大型项目中,随着事件数量增加,事件总线的可维护性会下降,此时应选择 Vuex/Pinia/Redux 等状态管理库。

总之,技术没有优劣之分,只有适用场景的不同。合理选择事件总线或状态管理库,才能让项目的组件通信更高效、更易维护。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

canjun_wen

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

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

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

打赏作者

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

抵扣说明:

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

余额充值