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


组件通信噩梦:我是如何用 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,从组件实例上移除了 o n , on, on,off, o n c e 这几个方法,只保留了 once 这几个方法,只保留了 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 支持和更规范的数据流。

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

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒8 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端