实现事件总线结合Vue应用

手写一个事件总线(EventBus)并在Vue应用中实现组件间通信

问题

在Vue应用中,组件间通信是常见需求。对于非父子组件或跨层级组件通信,事件总线(EventBus)是一种轻量级的解决方案。本题要求实现一个事件总线系统,支持事件的订阅、发布和取消订阅,并展示如何在Vue应用中使用。

解答

// EventBus.js - 事件总线实现
class EventBus {
  constructor() {
    // 存储事件及其对应的回调函数
    this.events = {};
  }

  /**
   * 订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   * @param {Object} context - 回调函数的上下文(this指向)
   */
  on(eventName, callback, context = null) {
    if (!eventName || typeof callback !== 'function') {
      console.warn('事件名称和回调函数不能为空');
      return;
    }

    // 如果事件不存在,创建一个新数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }

    // 添加回调函数和上下文
    this.events[eventName].push({
      callback,
      context
    });
  }

  /**
   * 订阅一次性事件(触发后自动取消订阅)
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   * @param {Object} context - 回调函数的上下文
   */
  once(eventName, callback, context = null) {
    // 包装回调函数,执行后自动取消订阅
    const wrappedCallback = (...args) => {
      callback.apply(context, args);
      this.off(eventName, wrappedCallback);
    };

    this.on(eventName, wrappedCallback, context);
  }

  /**
   * 触发事件
   * @param {string} eventName - 事件名称
   * @param  {...any} args - 传递给回调函数的参数
   */
  emit(eventName, ...args) {
    if (!this.events[eventName]) {
      return;
    }

    // 执行所有订阅该事件的回调函数
    this.events[eventName].forEach(({ callback, context }) => {
      callback.apply(context, args);
    });
  }

  /**
   * 取消订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 要取消的回调函数(可选)
   */
  off(eventName, callback = null) {
    if (!this.events[eventName]) {
      return;
    }

    // 如果没有指定回调函数,删除该事件的所有订阅
    if (!callback) {
      delete this.events[eventName];
      return;
    }

    // 删除指定的回调函数
    this.events[eventName] = this.events[eventName].filter(
      item => item.callback !== callback
    );

    // 如果该事件没有订阅者了,删除该事件
    if (this.events[eventName].length === 0) {
      delete this.events[eventName];
    }
  }

  /**
   * 清空所有事件订阅
   */
  clear() {
    this.events = {};
  }
}

// 导出单例
export default new EventBus();
// Vue 2.x 插件形式
// eventBusPlugin.js
import EventBus from './EventBus';

export default {
  install(Vue) {
    // 将事件总线挂载到 Vue 原型上
    Vue.prototype.$bus = EventBus;
  }
};
// Vue 3.x 插件形式
// eventBusPlugin.js (Vue 3)
import EventBus from './EventBus';

export default {
  install(app) {
    // 将事件总线挂载到全局属性上
    app.config.globalProperties.$bus = EventBus;
    
    // 也可以通过 provide 提供
    app.provide('$bus', EventBus);
  }
};

使用示例

// main.js - Vue 2.x
import Vue from 'vue';
import App from './App.vue';
import EventBusPlugin from './eventBusPlugin';

Vue.use(EventBusPlugin);

new Vue({
  render: h => h(App)
}).$mount('#app');
<!-- ComponentA.vue - 发送消息的组件 -->
<template>
  <div class="component-a">
    <h3>组件 A</h3>
    <input v-model="message" placeholder="输入消息" />
    <button @click="sendMessage">发送消息</button>
  </div>
</template>

<script>
export default {
  name: 'ComponentA',
  data() {
    return {
      message: ''
    };
  },
  methods: {
    sendMessage() {
      // 触发事件,传递数据
      this.$bus.emit('message-sent', {
        content: this.message,
        timestamp: Date.now()
      });
      this.message = '';
    }
  }
};
</script>
<!-- ComponentB.vue - 接收消息的组件 -->
<template>
  <div class="component-b">
    <h3>组件 B</h3>
    <div v-if="receivedMessage">
      <p>收到消息: {{ receivedMessage.content }}</p>
      <p>时间: {{ formatTime(receivedMessage.timestamp) }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ComponentB',
  data() {
    return {
      receivedMessage: null
    };
  },
  created() {
    // 订阅事件
    this.$bus.on('message-sent', this.handleMessage, this);
  },
  beforeDestroy() {
    // 组件销毁前取消订阅,防止内存泄漏
    this.$bus.off('message-sent', this.handleMessage);
  },
  methods: {
    handleMessage(data) {
      this.receivedMessage = data;
    },
    formatTime(timestamp) {
      return new Date(timestamp).toLocaleTimeString();
    }
  }
};
</script>
// 在 Composition API 中使用 (Vue 3)
import { onMounted, onUnmounted, getCurrentInstance } from 'vue';

export default {
  setup() {
    const instance = getCurrentInstance();
    const $bus = instance.appContext.config.globalProperties.$bus;

    const handleMessage = (data) => {
      console.log('收到消息:', data);
    };

    onMounted(() => {
      $bus.on('message-sent', handleMessage);
    });

    onUnmounted(() => {
      $bus.off('message-sent', handleMessage);
    });

    return {};
  }
};

关键点

  • 发布-订阅模式:事件总线基于发布-订阅模式,实现组件间的解耦通信

  • 事件存储结构:使用对象存储事件名和回调函数数组的映射关系,支持一个事件多个订阅者

  • 上下文绑定:保存回调函数的上下文(context),确保回调函数中的 this 指向正确

  • 一次性订阅once 方法通过包装回调函数,在执行后自动取消订阅

  • 内存泄漏防范:组件销毁时必须取消事件订阅(在 beforeDestroyonUnmounted 中调用 off

  • 灵活的取消订阅:支持取消指定回调或取消某个事件的所有订阅

  • Vue 集成方式:通过插件形式挂载到 Vue 原型或全局属性上,方便在组件中使用

  • 参数传递:使用剩余参数(...args)支持传递任意数量和类型的参数

  • 错误处理:添加基本的参数校验,提高代码健壮性

  • 单例模式:导出事件总线的单例实例,确保全局使用同一个实例