Vue 3|如何在兩個子元件傳遞函式與參數? Vue 必學套件 — Mitt!
2025 Vue 3.5.13+、Mitt 的基本使用方式,並以範例解決「事件完成後再執行路由跳轉」、Vue 3 跨元件溝通方式。
在 Vue 3 開發中,當專案變得複雜、元件層級較深時,單靠 props 傳遞與 emit 事件來進行溝通往往顯得冗長且不夠靈活。這時候,利用事件總線/事件匯流排(Event Bus)可以大幅簡化元件間的溝通流程,而 Mitt 就是一個輕量且高效套件。
這篇將介紹 Mitt 的基本使用方式,並以範例解決「事件完成後再執行路由跳轉」這類需求。٩(๑•̀ω•́๑)۶
✨為什麼選擇 Mitt?
輕量簡單:mitt 只有約 200B 的大小,無任何額外依賴,且 API 設計直觀,僅包含三個方法:
emit
:觸發事件on
:監聽事件off
:取消事件監聽
靈活性高:適合用於簡單的跨元件或全域事件傳遞,例如按鈕點擊、通知、快捷鍵等情況,而不必引入較重的狀態管理庫。
✨1. 安裝與設定 Mitt
npm install mitt
接著,在專案中建立一個事件模組(例如:src/utils/eventBus.ts
或 src/utils/eventBus.js
):
// src/utils/eventBus.ts
import mitt from 'mitt';
const eventBus = mitt();
export default eventBus;
這樣我們就可以在各個元件中引入,並使用這個事件總線/事件匯流排來進行溝通。
✨2. 基本事件傳遞
以兩個子元件 A 與 B 為例
📌2.1 在子元件 A 中監聽事件(on)
假設子元件 A 中有一個依賴自身狀態的函式 save()
,我們希望當接收到 save-mission
事件時執行這個函式。
<!-- 子元件 A -->
<script setup>
import { reactive, onMounted, onUnmounted } from 'vue';
import eventBus from '@/utils/eventBus';
const state = reactive({
userName: 'aaaaaaaaa'
});
const save = () => {
console.log('🚀 資料已儲存!');
console.log(state.userName);
};
// 為了避免元件重複渲染導致多次註冊事件 onMounted註冊 onUnmounted銷毀
onMounted(() => {
eventBus.on('save-mission', save);
});
onUnmounted(() => {
eventBus.off('save-mission', save);
});
</script>
📌2.2 在子元件 B 中觸發事件( emit )
子元件 B 需要觸發 save-mission
事件,以讓子元件 A 執行 save()
。
當使用者點擊按鈕時,B 元件便 emit 該事件:
<!-- 子元件 B -->
<script setup>
import eventBus from '@/utils/eventBus';
const triggerSave = () => {
eventBus.emit('save-mission');
};
</script>
<template>
<button @click="triggerSave">儲存</button>
</template>
✨3. 事件完成後再執行下一步操作
在某些情形下,可能需要確保事件處理完成後再執行後續邏輯(例如:先儲存資料,再關閉 Modal 或進行路由跳轉)。
如果 save()
是個 async
函式,直接 emit 事件,沒有辦法等待內部的 await 完成。
📌使用 Promise 搭配 resolve
解決方式是讓 emit 時傳遞一個 resolve
回調函式,待 save()
執行完後呼叫它,這樣可以讓呼叫端透過 Promise 確認事件處理完成:
<script setup>
import { useRouter } from 'vue-router';
import eventBus from '@/utils/eventBus';
const router = useRouter();
const saveAndNavigate = async () => {
await new Promise<void>((resolve) => {
// 傳遞 resolve 讓事件處理完畢後回應
eventBus.emit('save-mission', resolve);
});
// 儲存完成後進行路由跳轉
router.push({ name: 'SchedulePage' });
};
</script>
<template>
<button @click="saveAndNavigate">儲存並跳轉</button>
</template>
在子元件 A 中,對應的監聽函式需要接收這個 resolve
,並在完成保存操作後呼叫它:
<script setup>
import { onMounted, onUnmounted } from 'vue';
import eventBus from '@/utils/eventBus';
const save = async (resolve) => {
console.log('🚀 資料儲存中...')
// 模擬 async 操作
await new Promise((r) => setTimeout(r, 1000))
console.log('🚀 資料已儲存!')
// 如果有 resolve 函式,則回傳保存完成
if (resolve && typeof resolve === 'function') {
resolve()
}
}
onMounted(() => {
eventBus.on('save-mission', save);
});
onUnmounted(() => {
eventBus.off('save-mission', save);
});
</script>
這樣的設計確保了在資料儲存完成之後,才會繼續執行後續邏輯。
✨4. 注意事項
📌4.1 正確使用 onMounted 與 onUnmounted
當元件會被頻繁建立與銷毀(例如 Modal、動態元件)時,必須在 onMounted
時註冊事件監聽,並在 onUnmounted
時移除,避免同一個事件被多次觸發或造成記憶體洩漏。
若元件是全域使用(例如 App.vue 中的元件),則可以在 setup 內部直接註冊,因為它只會執行一次。
📌4.2 記憶體管理
由於 mitt 的監聽需要手動移除,因此在元件銷毀時務必調用 off()
,以避免因重複註冊或未釋放監聽器而佔用不必要的資源。
📌4.3 小撇步
- 若只是要用到該元件的 function,不要它的UI,可以這樣使用
import Test from '@/components/test/Test.vue'
const isTestMounted = ref(true)
...
<Test v-if="isTestMounted" />
...
- 如果是該 ‘頁面’ 的 function,只要在 router 有配置過,或是 App.vue 有引入,就不需要額外處理 import。
✨5. Mitt 與其他狀態管理方案的比較
1. Mitt 適用短暫的事件傳遞
優點:輕量又簡單
缺點:太過簡單(?),如果遇到複雜的邏輯比較難處理、需要手動 unmonut。
2. Vuex/Pinia 適合複雜的狀態管理(使用者登入、全域變數、專案共享的value)
優點:官方推薦、結構化管理
缺點:學習曲線較高、如果是上面的案例,會將過多不必要的 state 變成全域變數,成本過高。
3. Props/Emit 適合父子元件之間的資料傳遞
優點:Vue 官方設計
缺點:Props drilling...🥸🥸🥸( ´•̥̥̥ω•̥̥̥` )🥸🥸🥸
📌何時選擇 Mitt:
- 適用情況:若只需要在局部或全域傳遞簡單事件(例如點擊、通知、鍵盤快捷鍵),並且不牽涉大量狀態管理,mitt 是一個不錯的選擇。
- 不適用情況:若需要複雜的狀態共享或雙向綁定,則建議使用 Vuex 或 Pinia;若需要事件回應(例如從 API 獲取回傳結果),則可能需要搭配 Promise 或其他回調機制來確保流程完整性。
結論
這個套件真的是相見恨晚,因為接手 Vue3 專案時間還不長,之前同事遇到這種情況的寫法是把所有用到的 state ,改全域變數,然後把 function 複製到要用的新元件內(😵😵😵🤬🥲),有了 Mitt 就完全可以避免這種重工的情形~~~~ 灑花🎉🎉🎉🎉🎉