目錄
- 一、原理
- 二、使用方法
- 三、優缺點
- 優點
- 缺點
- 四、使用注意事項
- 具體代碼
- 參考:
一、原理
在Vue中,事件總線(Event Bus)是一種可實現任意組件間通信的通信方式。
要實現這個功能必須滿足兩點要求:
(1)所有組件都能看到它;
(2)可以進行事件的監聽;
對于第一個要求,Vue通過內置關系VueComponent.prototype.__proto__ === Vue.prototype,使組件實例對象可以訪問到Vue原型上的屬性和方法,所以只需把事件總線放在Vue的原型對象上,它就可以被所有組件訪問。
至于第二個要求,Vue在原型對象上定義了$on、$emit、$off等方法,用于實現事件監聽。基本原理是基于消息訂閱發布:調用$on方法時,將函數存在以事件名為key的數組里;當調用$emit時,獲取相應數組里的所有函數,并逐個執行。
源碼位置:https://github.com/vuejs/vue/blob/main/src/core/instance/events.ts
所以事件總線就是一個定義在Vue原型對象上的Vue實例。
new Vue({el: '#app',render: h => h(App),beforeCreate() {Vue.prototype.$bus = this;}
})
二、使用方法
一個A組件想給B組件發送數據的場景:
A組件:
// A組件想發送數據,則觸發事件并傳遞參數,參數可以是零個到多個
this.$bus.$emit(事件名, 參數);
B組件:
// B組件想接收數據,則在B組件中給$bus綁定自定義事件,事件的回調留在B組件自身
this.$bus.$on(事件名, 回調函數);
// 需要在beforeDestory鉤子中解綁事件,避免內存泄露
this.$bus.$off(事件名, 回調函數);
三、優缺點
優點
- 任意組件間通信
- 組件解耦
通過使用事件總線,可以將組件之間的直接依賴關系解耦,使組件更加獨立和可復用。組件只需要關注自身的功能,而不用關心其他組件的實現細節。
缺點
- 只能被動接收數據,不能隨時獲取狀態
如果需要隨時獲取狀態,可以使用狀態管理工具Vuex或者Pinia。 - 代碼難以調試、數據流向難以追蹤
在大型應用中,事件總線的濫用可能導致組件之間的關系變得混亂,導致追蹤代碼執行流程和調試變得更加困難。 - 潛在的性能問題
大量的全局事件監聽和觸發可能導致性能問題,尤其是在頻繁觸發事件的情況下。
四、使用注意事項
- 避免事件命名沖突
由于事件總線是一個全局的對象,為避免事件命名沖突導致錯誤觸發/解綁事件,建議使用具唯一性的命名空間前綴區分不同的事件。 - 避免錯誤解綁事件
在解綁事件時,一定要帶上事件名和相應的回調函數,否則可能會錯誤解綁其他組件的事件,影響其他組件的正常運行。
// 僅將回調函數與事件名解綁
this.$bus.$off(事件名, 回調函數);
// 解綁事件名下的所有事件
this.$bus.$off(事件名);
// 解綁所有事件
this.$bus.$off();
- 回調函數不能是匿名函數
匿名函數會導致事件無法正常被解綁
- 沒有解綁事件,可能導致內存泄露
當組件被銷毀時,相關DOM結點已經從DOM樹分離出來了,但是還有綁定的事件指向它,導致這些DOM結點無法被垃圾回收,一直在內存里面,就會引發內存泄露。
所以在組件銷毀時,可以在組件的beforeDestroy鉤子中使用$off方法解綁事件,以防止內存泄漏。
沒有解綁時:
正常解綁時:
- 注意事件的傳參
在發送事件時,可以通過參數傳遞數據。但請確保傳遞的數據是簡單且不可變的,避免直接傳遞引用類型的數據,以免造成數據不一致或意外的修改。
receive(receiveData) {this.receiveData = receiveData;// 在接收的組件中修改了對象中的值,發送的組件中的值也會改變this.receiveData.text = '111';},
- 慎用全局事件
全局事件有很大的便利性,但也容易造成不可預測的問題。在使用事件線時,盡量避免濫用全局事件,可以考慮使用更明確的通信方式。
(1)props和自定義事件應該是父子間通信的首選;
(2)兄弟節點通信可通過它們的父節點進行;
(3)隔代組件通信可以使用provide/inject。它可以避免“prop逐級透傳”問題,即prop需要通過許多層級的組件傳遞下去,但這些組件本身可能并不需要那些prop。
(4)全局共享的數據管理,一般使用Pinia或Vuex等工具。
具體代碼
main.js
import Vue from 'vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'Vue.config.productionTip = false
Vue.use(ElementUI)new Vue({el: '#app',render: h => h(App),beforeCreate() {Vue.prototype.iBus = this;}
})
App.vue(用v-if就可以觸發組件銷毀,不一定要用tab)
<template><div><el-tabs v-model="editableTabsValue" type="card" closable @tab-remove="closeTab"><el-tab-panev-for="item in editableTabs":key="item.name":label="item.title":name="item.name"><send-tab v-if="item.name === 'sendTab'"/><receive-tab v-else/></el-tab-pane></el-tabs></div>
</template><script>import ReceiveTab from "@/components/ReceiveTab.vue";
import SendTab from "@/components/SendTab.vue";export default {name: 'App',components: {SendTab, ReceiveTab},data() {return {editableTabsValue: 'sendTab',editableTabs: [{title: '發送頁面',name: 'sendTab',},{title: '接收頁面',name: 'receiveTab',},],tabIndex: 1,}},methods: {closeTab(targetName) {let tabs = this.editableTabs;let activeName = this.editableTabsValue;if (activeName === targetName) {tabs.forEach((tab, index) => {if (tab.name === targetName) {let nextTab = tabs[index + 1] || tabs[index - 1];if (nextTab) {activeName = nextTab.name;}}});}this.editableTabsValue = activeName;this.editableTabs = tabs.filter(tab => tab.name !== targetName);}}
}
</script>
SendTab.vue
<script>
export default {name: "SendTab",data() {return {input: {text: '',},}},methods: {send() {console.log('--------------emit', this.iBus._events)this.iBus.$emit('bus-demo', this.input, 'data2');},},beforeDestroy() {console.log('--------------發送組件的beforeDestroy')},
}
</script><template><div><el-input style="width: 250px" v-model="input.text"/><el-button type="primary" @click="send">發送數據</el-button></div>
</template>
ReceiveTab.vue
<script>
export default {name: "ReceiveTab",data() {return {receiveData: null,}},created() {this.iBus.$on('bus-demo', this.receive);// this.iBus.$on('anon-func', () => {// console.log('---------------匿名函數')// });console.log('--------------on', this.iBus._events)},beforeDestroy() {this.iBus.$off('bus-demo', this.receive);// this.iBus.$off('anon-func', () => {// console.log('---------------匿名函數')// });console.log('--------------接收組件的beforeDestroy')},methods: {receive(receiveData, otherData) {this.receiveData = receiveData;// this.receiveData.text = '111';console.log('---------第二個參數', otherData)},},
}
</script><template><span>接收到的數據:{{ receiveData }}</span>
</template>
參考:
- Vue3遷移指南-事件總線
- 尚硅谷Vue教程-84/85事件總線
- 【Vue知識】$on和$emit的實現原理
- 詳解Vue事件總線的原理與應用:EventBus
- 一個Vue頁面的內存泄露分析