组件通信噩梦:我是如何用 几 行代码“手搓”一个事件总线解决的


组件通信噩梦:我是如何用 50 行代码"手搓"一个事件总线解决的?

嘿,各位同学,今天咱们聊一个纯粹的、经典的、能解决大问题的设计模式------事件总线(Event Bus)。这玩意儿,在我还是个新手的时候,帮我摆平了一个至今记忆犹新的"组件通信噩梦",而且实现它的核心代码,可能比你想象的还要简单。

噩梦的开始:一个"各自为政"的后台布局

故事发生在一个典型的后台管理系统中。它的布局很常见:一个顶部的 Header,一个左侧的 Sidebar,和一个右侧的主内容区 MainContent。它们三者是兄弟组件 ,都被挂在 App.vue 下。

很快,需求来了:

  1. 用户在 Header 里点击了"切换项目"的下拉菜单,Sidebar 的菜单列表和 MainContent 的数据看板都需要立即刷新,以展示新项目的数据。
  2. 用户在 Sidebar 深处的一个设置菜单里,切换了"主题模式"(比如日间/夜间),HeaderMainContent 的背景色和字体颜色都得跟着变。

我当时就头大了 🤯。HeaderSidebar 怎么直接通信?难道要用 this.$parent.$emitthis.$on?那代码就跟意大利面一样搅和在一起了!或者为了这么点事就上 Vuex?用它来管理"项目ID"还行,但"主题模式"这种纯 UI 状态也放进去,感觉有点杀鸡用牛刀,而且 store 会变得很杂乱。

我的组件之间,就像隔着好几堵墙的人在喊话,通信效率极低,而且姿势极其难看。

柳暗花明:几行代码的"中央广播站"

就在我准备硬着头皮写"面条代码"的时候,我想到了一个古老又优雅的模式:事件总线

把它想象成一个"中央广播站 "。任何组件都可以向这个广播站 $emit(发射)一个带有特定频道(事件名)的信号。而其他任何组件,只要提前 $on(收听)了该频道,就能接收到这个信号并做出反应。

于是,我花了十分钟,"手搓"了下面这个 event-bus.js

事件总线:

  1. 提供监听某个事件的接口
  2. 提供取消监听的接口
  3. 触发事件的接口(可传送数据)
  4. 触发事件后会自动通知监听者
javascript 复制代码
// event-bus.js

const listeners = {};

// 事件总线
export default {
  // 监听某一个事件
  $on(eventName, handler) {
    if (!listeners[eventName]) {
      // 关键点1: 使用 Set 而不是数组!
      listeners[eventName] = new Set();
    }
    listeners[eventName].add(handler);
  },

  // 取消监听
  $off(eventName, handler) {
    if (!listeners[eventName]) {
      return;
    }
    listeners[eventName].delete(handler);
  },

  // 触发事件
  $emit(eventName, ...args) {
    if (!listeners[eventName]) {
      return;
    }
    for (const handler of listeners[eventName]) {
      handler(...args);
    }
  },
};

第一次恍然大悟 💡:为什么用 new Set() 而不是用数组 [] 来存储事件的处理函数(handler)?

  • 唯一性Set 结构天然不允许重复值。这意味着同一个组件的同一个处理函数,即使不小心 $on 了好几次,也只会存一份。完美避免了"一个事件触发,函数执行多次"的诡异bug!
  • 性能Setdelete() 操作比数组的 splice()filter() 来删除指定元素要快得多。对于频繁注册和注销的场景,性能更优。

就这短短几行不到的代码,构建起了一个强大的、解耦的通信系统。

在Vue里面也可以这样写:

js 复制代码
import Vue from "vue";

export default new Vue({});

原因: 每个 Vue 实例都是一个天生的事件派发器,new Vue()创建了一个全新的、独立的 Vue 根实例。它没有 template,没有 data(只有一个空的 _data 对象),没有挂载到任何 DOM 元素上。

这里面就包括我们的三个核心方法(其实是四个): vm.$on(eventName, callback):监听当前实例上的自定义事件。

vm.$off(eventName, callback):移除自定义事件监听器。

vm.$emit(eventName, ...args):触发当前实例上的事件。

vm.$once(eventName, callback):监听一个自定义事件,但只触发一次,在第一次触发后监听器就会被移除。

new Vue() 作为事件总线,是 Vue 2.x 时代一个极其优雅的方案。但是,请注意,这个技巧在 Vue 3 中已经完全失效了! Vue 3 为了追求更轻量、更纯粹的组合式 API,从组件实例上移除了 <math xmlns="http://www.w3.org/1998/Math/MathML"> o n , on, </math>on,off, <math xmlns="http://www.w3.org/1998/Math/MathML"> o n c e 这几个方法,只保留了 once 这几个方法,只保留了 </math>once这几个方法,只保留了emit。官方的理由是,他们不希望大家过度依赖事件总线这种模式去处理跨组件通信,而是鼓励使用更明确的数据流(如 provide/inject)或更专业的全局状态管理(如 Pinia)。

实战演练:当组件学会了"收听广播"

有了这个广播站,我们来解决之前的难题。

场景一:切换项目

Header.vue 里,当用户切换项目时,我们向外"广播"一个信号。

javascript 复制代码
// Header.vue
import eventBus from '@/utils/event-bus.js';

export default {
  methods: {
    onProjectChange(newProjectId) {
      // ...处理 Header 自己的逻辑...
      // 发射信号!频道是 'project-changed',附带的数据是 newProjectId
      eventBus.$emit('project-changed', newProjectId); 
    }
  }
}

现在,Sidebar.vueMainContent.vue 只需要"收听"这个频道就行了。

javascript 复制代码
// Sidebar.vue
import eventBus from '@/utils/event-bus.js';

export default {
  created() {
    // 组件创建时,开始收听 'project-changed' 频道
    eventBus.$on('project-changed', this.handleProjectChange);
  },
  methods: {
    handleProjectChange(projectId) {
      console.log('Sidebar 收到!新项目ID:', projectId);
      // ...执行刷新菜单列表的逻辑...
    }
  },
  // ...别忘了后面要讲的销毁监听!
}

MainContent.vue 的逻辑也几乎一样。你看,组件之间完全不知道对方的存在,却通过事件总线实现了完美协作,代码优雅得一塌糊涂。✅

最大的"坑":那个被遗忘的监听器 🐛

正当我为自己的杰作沾沾自喜时,我发现了测试时的一个问题:当我反复切换路由,离开再回到有监听的页面时,有时一个事件会导致好几次重复的响应!

💡$on,就必须有 $off

当一个组件(比如 Sidebar.vue)被销毁时,它在 created 里注册的 handleProjectChange 监听函数,依然存在于 eventBuslisteners 里!它就像一个幽灵 👻,赖着不走。当你再次创建这个组件时,又会注册一个新的监听。日积月累,listeners 里的"幽灵"越来越多,导致一次 $emit,多个"幽灵"和一个"活人"同时响应,不仅造成性能浪费,还可能引发各种奇怪的 bug。

正确的姿势是在组件销毁前,把自己的监听器给清理掉:

javascript 复制代码
// Sidebar.vue (修正版)
import eventBus from '@/utils/event-bus.js';

export default {
  created() {
    eventBus.$on('project-changed', this.handleProjectChange);
  },
  beforeDestroy() {
    // 组件销毁前,取消监听,还 eventBus 一个干净!
    eventBus.$off('project-changed', this.handleProjectChange);
  },
  methods: {
    handleProjectChange(projectId) {
      // ...
    }
  }
}

记住这个铁律:$on$off 必须成对出现,通常是在 created/mountedbeforeDestroy 这两个生命周期钩子中配对。

总结

这个小小的事件总线,虽然不是什么高深的技术,但它体现了一种重要的编程思想------发布-订阅模式

  • 何时用它 :当你需要在两个或多个没有直接父子关系 的组件之间传递消息,且这个通信不那么频繁、不涉及复杂的全局状态管理时,事件总线是一个轻量级且极度优雅的选择。
  • 何时不用它:如果你的应用有大量、复杂、高频的全局状态共享,那还是老老实实用 Vuex 或 Pinia 吧。它们提供了更强大的状态管理、dev-tools 支持和更规范的数据流。

希望我这次从"噩梦"到"解脱"的经历,能帮你把"事件总线"这个利器收入囊中。下次再遇到棘手的组件通信问题,你也能自信地"手搓"一个来解决它!

相关推荐
匀泪13 分钟前
网络安全初级(前端页面的编写分析)
前端·安全·web安全
林太白16 分钟前
Rust用户信息
前端·后端·rust
盏茶作酒2921 分钟前
自己实现Promise.all
前端
極光未晚38 分钟前
从卡顿到丝滑:我给 React 项目「踩油门」的那些事
前端·react.js·性能优化
Hilaku44 分钟前
这几个CSS和JS新特性,将在2026年变流行
前端·javascript·css
拾光拾趣录44 分钟前
如何高效判断DOM元素是否进入可视区域
前端·性能优化·dom
阿白的白日梦1 小时前
为 Vue3 + TypeScript + Vite 项目配置 Prettier代码格式化
前端
外啫啫1 小时前
基于n-scrollbar,滚动到列表指定位置
前端
好奇de悟空1 小时前
复合二进制文档 - 文档结构提取(中篇)
前端·javascript
好奇de悟空1 小时前
复合二进制文档 - msi文件信息提取(下篇)
前端·javascript