註冊mitt
main.ts註冊
import { createApp } from 'vue';
import App from './App.vue';
// 導入mitt
import mitt from 'mitt';
const app = createApp(App);
app.config.globalProperties.$mitt = mitt();
app.mount('#app');
封裝管理器
// 定義事件類型映射
export const mittEvents = {
EVENT_TEST: 'event:test', // 測試事件
};
/**
* 卸載監聽事件
* emit 只是觸發事件,不涉及監聽器的註冊,所以只需要移除監聽即可
* @param mitt mitt實例
* @param eventList 事件列表
*/
export const unloadMitt = (mitt: any, eventList: { eventName: string; callback?: (...args: any[]) => void }[]) => {
eventList.forEach(({ eventName, callback }) => {
if (callback) {
mitt.off(eventName, callback);
} else {
mitt.off(eventName);
}
});
};
定義事件類型映射的作用是用來做全局的事件映射和處理,如果你在頁面中隨意的派發和監聽emit事件,會導致事件混亂,意圖不明確,所以我們需要統一在某個位置集中管理事件,至少知道對應的事件的作用是什麼。
卸載監聽事件的作用是用來卸載對某個事件的on監聽,這是很關鍵的一步操作,如果不卸載的話可能會造成on事件的重複監聽,下面我來詳細説明一下:
1、組件內使用mitt.on,在銷燬組件時需要卸載,根據具名函數卸載
2、頁面級別的mitt.on也需要卸載,在onBeforeUnmount或者onUnmounted內卸載
全局監聽與卸載
頁面使用:
觸發端
import { getCurrentInstance } from "vue;
import { mittEvents, unloadMitt } from '@/utils/mittManage';
const { proxy } = getCurrentInstance() as any;
const onTest = () => {
proxy.$mitt.emit(mittEvents.EVENT_TEST, { text: '發送事件' });
};
接收端
import { getCurrentInstance } from "vue;
import { mittEvents } from '@/utils/mittManage';
const { proxy } = getCurrentInstance() as any;
proxy.$mitt.on(mittEvents.EVENT_TEST, (data: any) => {
console.log('接收事件', data);
});
使用的時候需要注意,vue的頁面都可以看成是一個組件,如果組件 渲染 -> 卸載 -> 渲染 (包括組件卸載、頁面未keep-alive的跳轉)後,mitt.on事件會被反覆創建,如果你沒有使用mitt.off卸載上一次的mitt.on監聽的話,下一次渲染又會再次創建一個mitt.on監聽,這樣創建了多個mitt.on監聽會導致同一個mitt.emit派發,mitt.on重複執行
正確的做法是在你mitt.on的頁面裏,在onBeforeUnmount或onUnmounted生命週期內卸載對應的監聽,比如這樣:
import { getCurrentInstance, onBeforeUnmount } from "vue;
import { mittEvents, unloadMitt } from '@/utils/mittManage';
const { proxy } = getCurrentInstance() as any;
proxy.$mitt.on(mittEvents.EVENT_TEST, (data: any) => {
console.log('接收事件', data);
});
// 卸載監聽
onBeforeUnmount(() => {
unloadMitt(proxy.$mitt, [{eventName: mittEvents.EVENT_TEST}]);
});
這樣,當你的頁面卸載掉後,當前的監聽也卸載了。
注意:在上面的示例中,mitt.on是匿名函數監聽,並且卸載時也僅提供了事件名,這會導致項目中所有對應的事件名一併卸載,如果你在不同的頁面引入了相同的組件,在該組件內卸載了監聽,會導致其它頁面無法使用,因為監聽被統一卸載掉了。
只卸載當前組件監聽(推薦)
我們有時候需要把一個組件當成一個獨立的環境,我們卸載監聽時,只需要卸載當前組件的監聽,不影響其它位置,此時可以用具名函數只移除當前組件的監聽器,如下:
import { getCurrentInstance, onBeforeUnmount } from "vue;
import { mittEvents, unloadMitt } from '@/utils/mittManage';
const { proxy } = getCurrentInstance() as any;
const eventTest = (data: any) => {
console.log('接收事件', data);
proxy.$mitt.on(mittEvents.EVENT_TEST, eventTest);
// 卸載監聽
onBeforeUnmount(() => {
unloadMitt(proxy.$mitt, [{eventName: mittEvents.EVENT_TEST, callback: eventTest}]);
});
在事件系統中(如 mitt 或類似的事件總線),監聽器的識別基於函數引用,而不是函數名或內容。
使用具名函數時:
eventTest是一個固定的函數引用- 每個組件實例都有自己的
eventTest函數實例 - 使用
mitt.off(eventName, event)可以精確移除特定的監聽器函數 - 不會影響其他組件註冊的監聽器
具名函數與匿名函數的區別
// 匿名函數 - 每次都是不同的引用
() => {} === () => {} // false
// 具名函數 - 在同一作用域內是相同引用
function handler() {}
handler === handler // true
const handler2 = () => {}
handler2 === handler2 // true
匿名函數:每次創建都是不同的函數實例,無法精確指定要移除哪一個
具名函數:在同一作用域內保持相同的引用,可以精確指定要移除的監聽器
這就是為什麼使用具名函數並配合 mitt.off(eventName, event)可以只移除當前組件的監聽器,而不會影響其他組件的原因。