Vue 全局事件总线详解

一、什么是事件总线

1.1 定义

事件总线 (Event Bus)本质上就是一个居中转发消息的"邮局":发送方不直接找接收方,而是把消息丢给总线,总线再帮转给所有订阅了这个消息的人。

在 Vue 里,它用来解决任意两个组件之间通信的问题,不限于父子,不限于兄弟,只要挂在同一条总线上就能互相收发。

1.2 生活类比

把事件总线想象成广播电台:

  • 你想听某个节目 → 先去电台登记订阅(订阅事件)
  • 主持人想播报 → 把内容交给电台 ($emit)
  • 电台把内容推给所有登记过的人

主持人和听众互相不认识 ,也不需要知道对方在哪,只跟电台打交道。这就是事件总线的核心思想:解耦发送方和接收方。

1.3 为什么要用

Vue 组件通信的"原生"方式有局限:

通信方式 限制
props / emits 只能父子,跨层级要一层层透传(Prop Drilling)
$refs 父直接操作子,反向不行,兄弟也不行
provide/inject 父给后代单向,后代回父不直接
Vuex / Pinia 适合管全局状态,小事件通知有点重

事件总线就填了这些空缺:任意两个组件都能对话,写法还特别轻 。适合做"开关侧边栏"、"切换主题"、"用户登出广播"这种一次性通知类需求。


二、Vue 2 的实现:用 Vue 实例当总线

2.1 核心思路

Vue 2 里,每个 Vue 实例都自带事件接口 $on / $emit / $off / $once。所以新建一个空的 Vue 实例当总线就行,谁需要谁引入。

javascript 复制代码
// src/eventBus.js
import Vue from "vue";

const bus = new Vue();   // ← 一个空实例,啥都没有,但有事件接口
export default bus;

2.2 基本用法:订阅 + 发布

javascript 复制代码
// 组件A:订阅"login"事件
import bus from "@/eventBus";

export default {
    created() {
        bus.$on("login", (user) => {
            console.log("收到登录:", user);  // ← 任何组件 emit 都能收到
        });
    },
    beforeDestroy() {
        bus.$off("login");   // ← 必须解绑,否则内存泄漏
    }
}
javascript 复制代码
// 组件B:发布"login"事件
import bus from "@/eventBus";

export default {
    methods: {
        doLogin() {
            bus.$emit("login", { name: "张三" });   // ← 不用管谁在听
        }
    }
}

A 和 B 可能隔着十层组件,可能根本不是父子,但只要都引入了 bus,消息就能传过去。

2.3 四个 API 速查

方法 作用 用法
$on(event, fn) 订阅事件 bus.$on("hi", fn)
$emit(event, ...args) 发布事件,可带参数 bus.$emit("hi", 1, 2)
$off(event, fn) 解绑;不传 fn 解绑该事件全部 bus.$off("hi") 或 bus.$off("hi", fn)
$once(event, fn) 订阅一次,触发后自动解绑 bus.$once("init", fn)

三、Vue 3 的实现:mitt 或手写

3.1 为什么 Vue 2 的写法不行了

Vue 3 移除了实例上的 $on / $off / $once (因为想跟组件实例解耦,推荐用 props/emits 显式通信)。所以 new Vue() 当总线这条路彻底断了,需要换工具。

3.2 方案一:用 mitt(推荐)

mitt 是一个超轻量的事件总线库,~200 字节,API 和 Vue 2 的几乎一样。

bash 复制代码
npm install mitt
javascript 复制代码
// src/eventBus.js
import mitt from "mitt";

const bus = mitt();   // ← 创建总线
export default bus;
javascript 复制代码
// 组件A:订阅
import bus from "@/eventBus";
import { onMounted, onBeforeUnmount } from "vue";

export default {
    setup() {
        const onLogin = (user) => console.log("登录:", user);

        onMounted(() => bus.on("login", onLogin));
        onBeforeUnmount(() => bus.off("login", onLogin));

        return {};
    }
}
javascript 复制代码
// 组件B:发布
import bus from "@/eventBus";
export default {
    setup() {
        const doLogin = () => bus.emit("login", { name: "张三" });
        return { doLogin };
    }
}

mitt 的 API 对照:

mitt 方法 Vue 2 对应 备注
on(type, handler) $on 完全对应
emit(type, data) $emit 只能传一个 data,不能像 $emit 多参
off(type, handler) $off 同样支持只传 type 解绑全部
clear() 无 清空所有订阅

3.3 方案二:手写一个 EventEmitter

不想引入依赖,自己写也很短:

javascript 复制代码
// src/eventBus.js
class EventBus {
    constructor() {
        this.map = new Map();   // ← { type: [fn1, fn2, ...] }
    }
    on(type, fn) {
        if (!this.map.has(type)) this.map.set(type, []);
        this.map.get(type).push(fn);
    }
    emit(type, ...args) {
        const fns = this.map.get(type);
        if (!fns) return;
        fns.forEach(fn => fn(...args));
    }
    off(type, fn) {
        if (!fn) { this.map.delete(type); return; }   // ← 不传 fn,清空整个事件
        const fns = this.map.get(type);
        if (fns) this.map.set(type, fns.filter(f => f !== fn));
    }
    clear() {
        this.map.clear();
    }
}

export default new EventBus();

用法和 mitt 一模一样。手写版的好处是能看清原理 ,坏处是要自己保证 this 指向、处理 once 等边缘情况。

3.4 注解

1.mitt 的 emit 只能带一个参数

Vue 2 里可以 bus.$emit("hi", 1, 2, 3),mitt 只能 bus.emit("hi", { a:1, b:2 }),多个参数要打包成对象。这是设计差异,不是 bug。

2.为什么 Vue 3 推荐显式通信却还保留 mitt

显式通信(props/emits/provide)在父子链路上更清晰,但跨层级、跨业务模块时,层层透传成本太高。mitt 是为了这类"广播型"场景留的口子,不是替代 props。


四、实战案例:全局侧边栏开关

需求:一个按钮(在任何位置)点击后,打开/关闭侧边栏(也在任何位置)。

4.1 总线文件

javascript 复制代码
// src/eventBus.js
import mitt from "mitt";
export default mitt();

4.2 侧边栏组件:监听开关

vue 复制代码
<!-- SideBar.vue -->
<template>
    <aside v-show="visible">侧边栏内容</aside>
</template>

<script setup>
import bus from "@/eventBus";
import { ref, onMounted, onBeforeUnmount } from "vue";

const visible = ref(false);
const toggle = (v) => (visible.value = v);   // ← 收到指令就改

onMounted(() => bus.on("sidebar:toggle", toggle));
onBeforeUnmount(() => bus.off("sidebar:toggle", toggle));
</script>

4.3 触发按钮:发广播

vue 复制代码
<!-- MenuButton.vue -->
<template>
    <button @click="open">打开侧边栏</button>
</template>

<script setup>
import bus from "@/eventBus";

const open = () => bus.emit("sidebar:toggle", true);   // ← 不关心谁在听
</script>

按钮和侧边栏没有任何 props 约定 ,谁也不依赖谁。换一个按钮、加一个开关,只要 emit("sidebar:toggle", x) 就行。


相关推荐
涉密IT资质笔记2 小时前
涉密人员脱密期管理规范:期限分级模型、就业限制边界与违规认定标准
java·服务器·前端·网络·数据库
EatFan2 小时前
Go语言全栈实战:基于 Gin + Vue + JWT + RBAC 从零搭建前后端分离权限管理系统
vue.js·golang·go·vue·gin·jwt·rbac
u0111026752 小时前
工具页案例图如何编写 alt让图片说明与处理场景对应
java·前端·javascript·图像处理·人工智能·算法·ai作画
事圆则缓2 小时前
Flutter 状态管理框架对比(五):GetX 的响应式、依赖注册与生命周期
前端·javascript·flutter
广州智造3 小时前
Radioss复合材料使用的单元属性
前端·设计·cad·建模·cae·radioss
张宏宇3 小时前
我把微软开源的 tgrep 做成了本地版 GitHub Code Search:45,629 个文件里搜一次 10ms
前端·后端
陈拉斯3 小时前
Vue SPA 站百度收录“全军覆没“?一次预渲染抢救的完整实战(2026 踩坑版)
vue.js
huakoh3 小时前
MCP 调用超时后别急着重试:三步读回分清执行状态
前端
打工仔折腾 AI3 小时前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战