vue3-mitt-event-bus

test12026-10-2619 次阅读

为什么不直接用状态管理

父子、跨层级的数据流动用 provide/inject 或 Pinia 都很顺。但有些事件本质是一次性的通知:登录成功、购物车变化、全局 toast,它们不是长期状态,硬塞进 store 反而让状态变脏。一个轻量事件总线更适合这类场景,而 Vue 3 已移除内置的 $on/$off,社区普遍用 mitt 接手。

封装一个带类型的 emitter

mitt 支持泛型,把事件名和载荷类型写进一个接口,拼错事件名或传错字段都会在编译期报错。

import mitt from "mitt";

type Events = {
  "user:login": { id: number; name: string };
  "cart:change": number;
  "toast:show": { message: string; type: "success" | "error" };
};

export const emitter = mitt<Events>();

在组件里发布与订阅

订阅一定要在组件卸载时 off 掉,否则组件销毁后回调仍在,会访问到已释放的引用。

import { onMounted, onUnmounted } from "vue";
import { emitter } from "./emitter";

onMounted(() => {
  emitter.on("toast:show", showToast);
});

onUnmounted(() => {
  emitter.off("toast:show", showToast);
});

function showToast(payload: { message: string; type: "success" | "error" }) {
  console.log(payload.message);
}

// 任意模块里触发,无需关心谁在监听
function login() {
  emitter.emit("user:login", { id: 1, name: "alice" });
}

在组合式函数里自动清理

把 on/off 包一层类型安全的 helper,再放进 composable,组件只需声明自己关心哪个事件,卸载逻辑交给 onUnmounted。

import type { Handler } from "mitt";
import { onUnmounted } from "vue";
import { emitter, Events } from "./emitter";

function on<K extends keyof Events>(
  type: K,
  handler: (payload: Events[K]) => void
) {
  emitter.on(type, handler as Handler<Events[keyof Events]>);
}
function off<K extends keyof Events>(
  type: K,
  handler: (payload: Events[K]) => void
) {
  emitter.off(type, handler as Handler<Events[keyof Events]>);
}

export function useEventBus<K extends keyof Events>(
  type: K,
  handler: (payload: Events[K]) => void
) {
  on(type, handler);
  onUnmounted(() => off(type, handler));
}

注意点

  • 事件总线适合通知,不适合当全局状态用;需要读最新值的地方还是交给 store。
  • mitt 默认只存一个 handler 引用,所以 off 时传的函数必须和 on 时是同一个,建议用命名函数而非内联箭头。
  • 类型接口要随业务演进维护,事件越乱越要有文档。
T

test1

文章作者

为什么不直接用状态管理 父子、跨层级的数据流动用 provide/inject 或 Pinia 都很顺。但有些事件本质...

分类
技术
发布时间
2026-10-26
字数
约 1799 字
阅读
19 次