在 Vue3 生態中,組件通信是前端開發的核心需求之一。父子組件間的 props/emit 雖簡潔高效,卻難以應對跨層級、無關聯組件的通信場景;provide/inject 偏向全局狀態注入,缺乏靈活的事件通知能力;而 Pinia/Vuex 作為專門的狀態管理工具,對於臨時、非持久化的事件觸發又顯得過於笨重。

面對這些痛點,mitt 以其超輕量、無依賴、API 簡潔的特性,成為 Vue3 項目中實現靈活組件通信與全局事件調度的最優解之一。它不僅能輕鬆突破組件層級限制,實現任意組件間的消息傳遞,還能高效支撐全局通知、彈窗聯動等場景,兼顧性能與可維護性。本文將從核心原理、實戰落地、高級技巧到最佳實踐,全面拆解如何基於 mitt 搭建一套成熟的組件通信與全局事件方案,助力開發者高效解決項目中的通信難題。

一、為什麼選擇 mitt?

在 Vue3 項目中,組件通信是核心需求,但不同場景下的通信方案存在侷限:

  • 父子組件:props/emits 夠用,但跨多級組件(祖孫 / 叔侄)時冗餘;
  • Pinia/Vuex:適合全局狀態共享,但僅用於 “事件通知”(如全局通知、狀態變更回調)時過重;
  • Vue2 原生 EventBus:Vue3 已移除,且存在事件泄漏、命名衝突風險。

mitt 作為一款極簡的事件總線庫,完美解決這些痛點:

  1. 超輕量:體積僅 200B,無第三方依賴,不增加打包負擔;
  2. API 極簡:僅 on(監聽)、emit(觸發)、off(取消監聽)、all.clear(清空所有事件)4 個核心方法;
  3. 無框架依賴:可在 Vue/React/ 原生 JS 中使用,適配 Vue3  setup>;
  4. 支持多事件監聽:可同時監聽多個事件,支持通配符 '*' 監聽所有事件;
  5. 避免命名衝突:支持自定義事件名,可通過命名空間(如 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, '李四')
  1. 4.慎用 all.clear():

  2. 該方法會清空所有事件監聽,可能影響其他組件的正常功能,僅在特殊場景(如退出登錄時重置全局狀態)使用。

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>

效果:

Vue3 實用篇:基於 mitt 打造靈活的組件通信與全局事件方案_封裝

六、總結

mitt 作為一款極簡的事件總線庫,完美適配 Vue3 的開發模式,在跨組件通信、全局事件通知等場景中表現出色,既避免了 props/emits 多級傳遞的冗餘,又比 Pinia/Vuex 更輕量化。通過本文的封裝方案和實戰案例,你可以快速將 mitt 落地到項目中,解決組件通信的核心痛點。

此外,mitt 不僅限於 Vue3,還可用於 React、原生 JS 項目,學習成本低、複用性強。結合組合式函數封裝後,能進一步簡化代碼、提升開發效率,是 Vue3 項目中值得優先選擇的 “輕量級通信方案”。