在 Vue3 生態中,組件通信是前端開發的核心需求之一。父子組件間的 props/emit 雖簡潔高效,卻難以應對跨層級、無關聯組件的通信場景;provide/inject 偏向全局狀態注入,缺乏靈活的事件通知能力;而 Pinia/Vuex 作為專門的狀態管理工具,對於臨時、非持久化的事件觸發又顯得過於笨重。
面對這些痛點,mitt 以其超輕量、無依賴、API 簡潔的特性,成為 Vue3 項目中實現靈活組件通信與全局事件調度的最優解之一。它不僅能輕鬆突破組件層級限制,實現任意組件間的消息傳遞,還能高效支撐全局通知、彈窗聯動等場景,兼顧性能與可維護性。本文將從核心原理、實戰落地、高級技巧到最佳實踐,全面拆解如何基於 mitt 搭建一套成熟的組件通信與全局事件方案,助力開發者高效解決項目中的通信難題。
一、為什麼選擇 mitt?
在 Vue3 項目中,組件通信是核心需求,但不同場景下的通信方案存在侷限:
- 父子組件:props/emits 夠用,但跨多級組件(祖孫 / 叔侄)時冗餘;
- Pinia/Vuex:適合全局狀態共享,但僅用於 “事件通知”(如全局通知、狀態變更回調)時過重;
- Vue2 原生 EventBus:Vue3 已移除,且存在事件泄漏、命名衝突風險。
mitt 作為一款極簡的事件總線庫,完美解決這些痛點:
- 超輕量:體積僅 200B,無第三方依賴,不增加打包負擔;
- API 極簡:僅 on(監聽)、emit(觸發)、off(取消監聽)、all.clear(清空所有事件)4 個核心方法;
- 無框架依賴:可在 Vue/React/ 原生 JS 中使用,適配 Vue3 setup>;
- 支持多事件監聽:可同時監聽多個事件,支持通配符 '*' 監聽所有事件;
- 避免命名衝突:支持自定義事件名,可通過命名空間(如 user:login)區分事件。
二、環境準備
2.1 安裝 mitt
# npm
npm i mitt
# yarn
yarn add mitt
# pnpm
pnpm add mitt
或者CDN:
<!-- 引入 mitt -->
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
<!-- 全局可用 window.mitt -->
<script>
const emitter = mitt() // 初始化事件總線
</script>
2.2 全局初始化(推薦)
為了方便全局使用,避免重複創建實例,建議在項目中封裝全局事件總線:
// src/utils/eventBus.ts
import mitt from 'mitt'
// 創建 mitt 實例
const emitter = mitt()
// 導出核心方法(簡化使用)
export const eventBus = {
// 監聽事件:eventName 事件名,callback 回調函數
on: (eventName: string | symbol, callback: mitt.Handler) => emitter.on(eventName, callback),
// 觸發事件:eventName 事件名,params 傳遞的參數
emit: (eventName: string | symbol, ...params: any[]) => emitter.emit(eventName, ...params),
// 取消監聽:單個事件/所有事件
off: (eventName?: string | symbol, callback?: mitt.Handler) => {
if (eventName && callback) {
emitter.off(eventName, callback)
} else if (eventName) {
emitter.off(eventName) // 取消該事件的所有監聽
} else {
emitter.all.clear() // 清空所有事件監聽(謹慎使用)
}
}
}
export default eventBus
三、核心場景實戰
場景 1:跨組件通信(非父子 / 多級組件)
業務場景:頁面 A 中的按鈕點擊後,通知頁面 B 更新數據(如列表頁刪除數據後,詳情頁刷新)、全局通知提示(如登錄成功後,導航欄顯示用户信息)。
示例:列表頁 → 詳情頁 數據更新通知
1.列表頁(觸發事件):刪除數據後,觸發 list:delete 事件,傳遞刪除的 ID;
<!-- src/components/ListComponent.vue -->
<template>
<div class="list-container">
<div v-for="item in list" :key="item.id" class="list-item">
<span>{{ item.name }}</span>
<button @click="handleDelete(item.id)">刪除</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import emitter, { EVENT_NAMES } from '@/utils/eventBus'
// 列表數據
const list = ref([
{ id: 1, name: 'Vue3 基礎教程' },
{ id: 2, name: 'mitt 事件總線' },
{ id: 3, name: '組件通信實戰' }
])
// 刪除方法:觸發事件
const handleDelete = (id) => {
// 1. 本地刪除數據
list.value = list.value.filter(item => item.id !== id)
// 2. 觸發刪除事件,傳遞 id
emitter.emit(EVENT_NAMES.LIST_DELETE, id)
// 3. 觸發全局提示事件
emitter.emit(EVENT_NAMES.GLOBAL_NOTICE, {
title: '操作成功',
content: `ID: ${id} 的數據已刪除`,
type: 'success'
})
}
</script>
2.詳情頁(監聽事件):監聽 list:delete 事件,收到通知後更新自身數據:
<!-- src/components/DetailComponent.vue -->
<template>
<div class="detail-container">
<h3>當前詳情</h3>
<div v-if="currentItem">
<p>ID:{{ currentItem.id }}</p>
<p>名稱:{{ currentItem.name }}</p>
</div>
<div v-else class="empty-tip">該數據已被刪除</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import emitter, { EVENT_NAMES } from '@/utils/eventBus'
// 默認詳情數據
const currentItem = ref({ id: 2, name: 'mitt 事件總線' })
// 定義事件處理函數(需單獨提取,方便後續取消監聽)
const handleListDelete = (deleteId) => {
if (deleteId === currentItem.value?.id) {
currentItem.value = null
}
}
// 組件掛載時監聽事件
onMounted(() => {
emitter.on(EVENT_NAMES.LIST_DELETE, handleListDelete)
})
// 組件卸載時取消監聽(關鍵:避免內存泄漏)
onUnmounted(() => {
emitter.off(EVENT_NAMES.LIST_DELETE, handleListDelete)
})
</script>
3:全局提示組件(監聽全局事件)
<!-- src/components/GlobalNotice.vue -->
<template>
<div v-if="notice" class="notice-box" :style="{ background: noticeBg }">
<h4>{{ notice.title }}</h4>
<p>{{ notice.content }}</p>
<button @click="closeNotice">關閉</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import emitter, { EVENT_NAMES } from '@/utils/eventBus'
// 通知數據
const notice = ref(null)
// 根據通知類型設置背景色
const noticeBg = computed(() => {
switch (notice.value?.type) {
case 'success': return '#f0f9ff'
case 'error': return '#fff2f0'
default: return '#f8f9fa'
}
})
// 關閉通知
const closeNotice = () => {
notice.value = null
}
// 全局事件處理函數
const handleGlobalNotice = (noticeData) => {
notice.value = noticeData
// 3 秒自動關閉
setTimeout(() => {
notice.value = null
}, 3000)
}
// 監聽全局通知事件
onMounted(() => {
emitter.on(EVENT_NAMES.GLOBAL_NOTICE, handleGlobalNotice)
})
// 取消監聽
onUnmounted(() => {
emitter.off(EVENT_NAMES.GLOBAL_NOTICE, handleGlobalNotice)
})
</script>
<style scoped>
.notice-box {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 9999;
}
</style>
4.根組件:引入所有組件
<!-- src/App.vue -->
<template>
<div class="app-container">
<h1>Vue3 + mitt 組件通信實戰</h1>
<ListComponent />
<DetailComponent />
<GlobalNotice />
</div>
</template>
<script setup>
import ListComponent from './components/ListComponent.vue'
import DetailComponent from './components/DetailComponent.vue'
import GlobalNotice from './components/GlobalNotice.vue'
</script>
場景 2:組合式函數封裝(複用事件邏輯)
業務痛點:多個組件需要監聽同一類事件(如全局通知、主題切換),重複寫 onMounted/onUnmounted 監聽邏輯冗餘。
解決方案:封裝組合式函數 useEventBus,統一處理事件監聽與取消,簡化組件代碼。
1.封裝組合式函數:
// src/hooks/useEventBus.ts
import { onMounted, onUnmounted, Ref, ref } from 'vue'
import eventBus from '@/utils/eventBus'
/**
* 組合式函數:簡化事件總線的使用(自動在組件卸載時取消監聽)
* @param eventName 事件名
* @returns { data: Ref<any>, emit: (params: any[]) => void } - data 接收事件參數,emit 觸發事件
*/
export const useEventBus = >(eventName: string | symbol) => {
// 存儲事件傳遞的數據
const data = ref<T | null>(null)
// 監聽事件的回調
const handleEvent = (...params: T[]) => {
// 若傳遞多個參數,存儲為數組;否則存儲單個值
data.value = params.length > 1 ? params : params[0]
}
// 組件掛載時監聽事件
onMounted(() => {
eventBus.on(eventName, handleEvent)
})
// 組件卸載時取消監聽
onUnmounted(() => {
eventBus.off(eventName, handleEvent)
})
// 觸發事件的方法(可選,方便組件內部觸發)
const emit = (...params: any[]) => {
eventBus.emit(eventName, ...params)
}
return { data, emit }
}
2.組件中使用(簡化代碼):
.vue -->
notice" v-if="noticeData">
3>{{ noticeData.title }}</h3>
<p>{{ noticeData.content }}</p>
>
<script setup lang="ts">
import { useEventBus } from '@/hooks/useEventBus'
// 監聽全局通知事件(自動處理掛載/卸載監聽)
const { data: noticeData } = useEventBus; content: string }>('global:notice')
</script>
oped>
.notice {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
>
3.其他組件觸發通知:
任意組件 -->
<template>
@click="sendNotice">發送全局通知
</template>
setup lang="ts">
import { useEventBus } from '@/hooks/useEventBus'
const { emit } = useEventBus('global:notice')
const sendNotice = () => {
emit({
title: '温馨提示',
content: '您有一條新的消息待查看'
})
}
場景 3:通配符監聽所有事件(調試 / 全局日誌)
mitt 支持用 '*' 通配符監聽所有事件,適合全局日誌記錄、調試場景:
// src/utils/eventLog.ts
import eventBus from './eventBus'
// 監聽所有事件,打印日誌
eventBus.on('*', (eventName, ...params) => {
console.log(`[事件總線] 事件觸發:${String(eventName)},參數:`, params)
// 可選:上報日誌到後端(如重要事件統計)
// reportLog({ eventName, params, time: new Date().toISOString() })
})
四、避坑指南與最佳實踐
1.避免事件泄漏:
- 組件內監聽的事件,務必在 onUnmounted 中取消(使用本文封裝的 useEventBus 可自動處理);
- 全局監聽的事件(如 main.ts 中),需在應用卸載時(如路由守衞 onUnmounted)取消。
2.事件名規範
採用 模塊:事件 命名空間(如 user:login、list:delete),避免命名衝突;建議在項目中維護 eventNames.ts 常量文件,統一管理事件名:
// src/constants/eventNames.ts
exportconstEVENT_USER_LOGIN = 'user:login'
exportconstEVENT_LIST_DELETE = 'list:delete'
exportconstEVENT_GLOBAL_NOTICE = 'global:notice'
3.參數傳遞建議:
傳遞多個參數時,建議封裝成對象(便於擴展,避免參數順序混亂):// 推薦
eventBus.emit('user:update', { id: 1001, name: '李四' })
// 不推薦(參數順序變更易出錯)
eventBus.emit('user:update', 1001, '李四')
- 4.慎用 all.clear():
-
- 該方法會清空所有事件監聽,可能影響其他組件的正常功能,僅在特殊場景(如退出登錄時重置全局狀態)使用。
TypeScript 類型強化:定義事件名與參數類型的映射,提升類型推導體驗
// src/utils/eventBus.ts(擴展)
import mitt from'mitt'
// 定義事件名與參數類型的映射
typeEvents = {
'user:login': { id: number; name: string; avatar: string }
'list:delete': number
'global:notice': { title: string; content: string }
}
// 創建類型安全的 mitt 實例
const emitter = mitt>()
// 後續方法會自動推導事件名和參數類型
exportconst eventBus = {
on: emitter.on,
emit: emitter.emit,
off: emitter.off
}
五、實例demo:
結合mitt實現一個本地的demo:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue3 + Mitt 事件總線 Demo</title>
<!-- 引入 Vue3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 引入 Mitt CDN -->
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
.component {
border: 1px solid #eee;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.component h3 {
margin-top: 0;
color: #2c3e50;
}
button {
padding: 8px 16px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 8px;
}
button:hover {
background: #359469;
}
.notice {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 999;
}
.list-item {
margin: 8px 0;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
</head>
<body>
<h1>Vue3 + mitt 瀏覽器端 Demo</h1>
<!-- 列表組件容器 -->
<div class="component" id="list-component">
<h3>1. 列表組件(觸發事件)</h3>
<div id="list-container"></div>
</div>
<!-- 詳情組件容器 -->
<div class="component" id="detail-component">
<h3>2. 詳情組件(監聽事件)</h3>
<div id="detail-content"></div>
</div>
<!-- 通知組件容器 -->
<div id="notice-container"></div>
<script>
// 1. 初始化 mitt 事件總線(全局唯一)
const emitter = mitt()
// 2. 全局事件名常量(規範命名)
const EVENT_LIST_DELETE = 'list:delete'
const EVENT_GLOBAL_NOTICE = 'global:notice'
// 3. Vue3 應用實例(列表組件)
const ListApp = Vue.createApp({
data() {
return {
list: [
{ id: 1, name: 'Vue3 基礎教程' },
{ id: 2, name: 'mitt 事件總線' },
{ id: 3, name: '組件通信實戰' }
]
}
},
template: `
<div>
<div v-for="item in list" :key="item.id" class="list-item">
<span>{{ item.name }}</span>
<button @click="handleDelete(item.id)">刪除(觸發事件)</button>
</div>
</div>
`,
methods: {
handleDelete(id) {
// 刪除列表項
this.list = this.list.filter(item => item.id !== id)
// 觸發事件:傳遞刪除的 ID
emitter.emit(EVENT_LIST_DELETE, id)
// 同時觸發全局通知
emitter.emit(EVENT_GLOBAL_NOTICE, {
title: '刪除成功',
content: `ID: ${id} 的數據已被刪除`
})
}
}
})
// 掛載到列表容器
ListApp.mount('#list-container')
// 4. Vue3 應用實例(詳情組件)
const DetailApp = Vue.createApp({
data() {
return {
currentItem: { id: 2, name: 'mitt 事件總線' } // 默認顯示 ID=2 的詳情
}
},
template: `
<div>
<div v-if="currentItem">
<h4>當前詳情 Item</h4>
<p>當前詳情 ID:{{ currentItem.id }}</p>
<p>當前詳情名稱:{{ currentItem.name }}</p>
</div>
<div v-else style="color: #e74c3c;">⚠️ 該數據已被刪除</div>
</div>
`,
mounted() {
// 組件掛載時監聽事件
this.eventHandler = (deleteId) => {
if (deleteId === this.currentItem.id) {
this.currentItem = null // 匹配當前詳情 ID,更新視圖
}
}
emitter.on(EVENT_LIST_DELETE, this.eventHandler)
},
unmounted() {
// 組件卸載時取消監聽(避免內存泄漏)
emitter.off(EVENT_LIST_DELETE, this.eventHandler)
}
})
// 掛載到詳情容器
DetailApp.mount('#detail-content')
// 5. Vue3 應用實例(通知組件)
const NoticeApp = Vue.createApp({
data() {
return {
notice: null // 存儲通知數據
}
},
template: `
<div class="notice" v-if="notice">
<h4 style="margin: 0 0 8px 0;">{{ notice.title }}</h4>
<p style="margin: 0;">{{ notice.content }}</p>
<button @click="closeNotice" style="margin-top: 10px; background: #95a5a6;">關閉</button>
</div>
`,
methods: {
closeNotice() {
this.notice = null
}
},
mounted() {
// 監聽全局通知事件
this.noticeHandler = (noticeData) => {
this.notice = noticeData
// 3 秒後自動關閉
setTimeout(() => this.notice = null, 3000)
}
emitter.on(EVENT_GLOBAL_NOTICE, this.noticeHandler)
},
unmounted() {
emitter.off(EVENT_GLOBAL_NOTICE, this.noticeHandler)
}
})
// 掛載到通知容器
NoticeApp.mount('#notice-container')
</script>
</body>
</html>
效果:
六、總結
mitt 作為一款極簡的事件總線庫,完美適配 Vue3 的開發模式,在跨組件通信、全局事件通知等場景中表現出色,既避免了 props/emits 多級傳遞的冗餘,又比 Pinia/Vuex 更輕量化。通過本文的封裝方案和實戰案例,你可以快速將 mitt 落地到項目中,解決組件通信的核心痛點。
此外,mitt 不僅限於 Vue3,還可用於 React、原生 JS 項目,學習成本低、複用性強。結合組合式函數封裝後,能進一步簡化代碼、提升開發效率,是 Vue3 項目中值得優先選擇的 “輕量級通信方案”。