闡述一下 VUE中 eventbus 的原理
解答:
EventBus是消息傳遞的一種方式,基于一個(gè)消息中心,訂閱和發(fā)布消息的模式,稱為發(fā)布訂閱者模式。
on('name', fn)訂閱消息,name:訂閱的消息名稱, fn: 訂閱消息的函數(shù)(參數(shù)是接收的消息)
emit('name', args)發(fā)布消息, name:發(fā)布的消息名稱 , args:發(fā)布的消息
vue組件非常常見的有父子組件通信,兄弟組件通信。而父子組件通信就很簡(jiǎn)單,父組件會(huì)通過 props 向下傳數(shù)據(jù)給子組件,當(dāng)子組件有事情要告訴父組件時(shí)會(huì)通過 $emit 事件告訴父組件。如果兩個(gè)頁(yè)面沒有任何引入和被引入關(guān)系,該如何通信了?
vuex?那如果項(xiàng)目沒那么復(fù)雜,不需要類似Vuex這樣的庫(kù)來(lái)處理組件之間的數(shù)據(jù)通信呢,這時(shí)候就可以考慮Vue中的 事件總線 ,即 EventBus來(lái)通信。
EventBus 又稱為事件總線。在Vue中可以使用 EventBus 來(lái)作為溝通橋梁的概念,就像是所有組件共用相同的事件中心,可以向該中心注冊(cè)發(fā)送事件或接收事件,所以組件都可以上下平行地通知其他組件,但也就是太方便所以若使用不慎,就會(huì)造成難以維護(hù)的“災(zāi)難”,因此才需要更完善的Vuex作為狀態(tài)管理中心,將通知的概念上升到共享狀態(tài)層次。
寫的時(shí)候可能會(huì)碰到一些問題,嘿嘿,注意下發(fā)布/訂閱時(shí)機(jī),消息發(fā)布時(shí),直接發(fā)布給(通知)訂閱者,如果沒有訂閱者,就不發(fā)送。發(fā)布完畢了,再訂閱,沒用,只能等下次發(fā)布了,所以訂閱事件必須在發(fā)布之前。
eg: A發(fā)布消息,B訂閱消息
所以B頁(yè)面要成功訂閱消息的話,必須保證先加載B頁(yè)面,然后再加載A,先把訂閱者$on加載在內(nèi)存中,否則拿不到消息。
我是鋼筋我就必須刷新后先進(jìn)A再進(jìn)B。這就要先了解下Vue路由切換特性,vue路由切換時(shí),會(huì)先加載新的組件,等新的組件渲染好但是還沒有掛載前,銷毀舊的組件,之后掛載新組件。
新組件beforeCreate
↓
新組件created
↓
新組件beforeMount
↓
舊組件beforeDestroy
↓
舊組件destroyed
↓
新組件mounted
注意,在$emit時(shí),必須已經(jīng)$on,否則將無(wú)法監(jiān)聽到事件。
所以正確的寫法應(yīng)該是在需要接收值的組件的created生命周期函數(shù)里寫$on,在需要往外傳值的組件的destroyed生命周期函數(shù)函數(shù)里寫:
destroyed(){
eventBus.$emit('dataUpdate',data)
}
這樣寫,在舊組件銷毀的時(shí)候新的組件拿到舊組件傳來(lái)的值,然后在掛載的時(shí)候更新頁(yè)面里的數(shù)據(jù)。
一、這種倆頁(yè)面跳轉(zhuǎn)完全可以通過router傳參解決,感覺忙活一圈沒卵用;
二、B、C頁(yè)面訂閱,A發(fā)布,在進(jìn)入B或者C時(shí),上面那個(gè)destroyed的方法,必須是從A進(jìn)B頁(yè)面,那進(jìn)C時(shí)就沒了,而且也無(wú)法從mounted中直接獲取數(shù)據(jù)然后更改data數(shù)據(jù),有局限。
所以我感覺這個(gè)EventBus主要應(yīng)用場(chǎng)景應(yīng)該是,一個(gè)頁(yè)面中有多個(gè)組件,也可能組件套組件,這種情況用props和emit傳參太麻煩,用EventBus正好解決問題。
問:vue兄弟組件如何通訊?
1、讓兄弟組件通過一個(gè)共同的父組件做為通訊橋梁彼此通訊(props、$emit)。
2、EventBus
廢話少說(shuō)上代碼:
event-bus.js
import Vue from "vue";
export const EventBus = new Vue();
a.vue
<template>
<button @click="sendMsg()">發(fā)送</button>
</template>
<script>
import { EventBus } from "@assets/utils/event-bus";
export default {
methods: {
sendMsg() {
EventBus.$emit("aMsg", "來(lái)自A頁(yè)面的消息");
}
}
};
</script>
b.vue
<template>
<p>B頁(yè)面,A發(fā)送來(lái)的消息:{{ msg }}</p>
</template>
<script>
import { EventBus } from "@assets/utils/event-bus";
export default {
data() {
return {
msg: "defaultMsg"
};
},
mounted() {
EventBus.$on("aMsg", msg => {
// A發(fā)送來(lái)的消息
this.msg = msg;
});
}
};
</script>
上面就是 EventBus 的使用方法,是不是很簡(jiǎn)單。但每次使用 EventBus 時(shí)都需要在各組件中引入 event-bus.js 。事實(shí)上,我們還可以讓事情變得更簡(jiǎn)單一些。那就是創(chuàng)建一個(gè)全局的 EventBus 。
全局EventBus
main.js
...
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function() {
return EventBus;
}
}
});
...
a.vue
<template>
<button @click="sendMsg()">發(fā)送</button>
</template>
<script>
import { EventBus } from "@assets/utils/event-bus";
export default {
methods: {
sendMsg() {
EventBus.$emit("aMsg", "來(lái)自A頁(yè)面的消息");
this.$bus.$emit("nameOfEvent", {
name: "shy"
});
}
}
// destroyed() {
// EventBus.$emit("aMsg", "來(lái)自A頁(yè)面的消息destroyed");
// this.$bus.$emit("nameOfEvent", {
// name: "shy-destroyed"
// });
// }
};
</script>
b.vue
<template>
<p>BBB頁(yè)面,A發(fā)送來(lái)的消息:{{ msg }}</p>
</template>
<script>
import { EventBus } from "@assets/utils/event-bus";
export default {
data() {
return {
msg: "defaultMsg"
};
},
created() {
EventBus.$on("aMsg", msg => {
// A發(fā)送來(lái)的消息
this.msg = msg;
});
this.$bus.$on("nameOfEvent", $event => {
console.log($event);
});
}
};
</script>
插件掛載方式
1、定義一個(gè)統(tǒng)一事件管理器
// 插件掛載方式
import Vue from 'vue'
// Bus 就是一個(gè)Vue對(duì)象,因此Vue.$on: 監(jiān)聽`當(dāng)前實(shí)例`上的自定義事件
let Bus = new Vue()
let install = (Vue) => {
Vue.prototype.$bus = Bus
}
export default { install }
2、引用EventBus
import Vue from 'vue'
import Bus from '@/assets/js/eventBus'
Vue.use(Bus)
移除事件監(jiān)聽者
如果想移除事件的監(jiān)聽,可以像下面這樣操作:
import {
eventBus
} from './event-bus.js'
EventBus.$off('aMsg', {})
你也可以使用 EventBus.$off('aMsg') 來(lái)移除應(yīng)用內(nèi)所有對(duì)此某個(gè)事件的監(jiān)聽?;蛘咧苯诱{(diào)用 EventBus.$off() 來(lái)移除所有事件頻道,不需要添加任何參數(shù) 。
知識(shí)點(diǎn)
Vue.$on 監(jiān)聽 當(dāng)前實(shí)例 上的自定義事件。
事件可以由 vm.$emit 觸發(fā)。回調(diào)函數(shù)會(huì)接收所有傳入事件觸發(fā)函數(shù)的額外參數(shù)。
Vue.$off 移除自定義事件監(jiān)聽器。
vm.$off( [event, callback] )
如果沒有提供參數(shù),則移除所有的事件監(jiān)聽器;
如果只提供了事件,則移除該事件所有的監(jiān)聽器;
如果同時(shí)提供了事件與回調(diào),則只移除這個(gè)回調(diào)的監(jiān)聽器。
發(fā)布訂閱模式主要包含哪些內(nèi)容呢?
- 發(fā)布函數(shù),發(fā)布的時(shí)候執(zhí)行相應(yīng)的回調(diào)
- 訂閱函數(shù),添加訂閱者,傳入發(fā)布時(shí)要執(zhí)行的函數(shù),可能會(huì)攜額外參數(shù)
- 一個(gè)緩存訂閱者以及訂閱者的回調(diào)函數(shù)的列表
- 取消訂閱(需要分情況討論)
自己實(shí)現(xiàn)一個(gè) Observer 對(duì)象:
//用于存儲(chǔ)訂閱的事件名稱以及回調(diào)函數(shù)列表的鍵值對(duì)
function Observer() {
this.cache = {}
}
//key:訂閱消息的類型的標(biāo)識(shí)(名稱),fn收到消息之后執(zhí)行的回調(diào)函數(shù)
Observer.prototype.on = function (key,fn) {
if(!this.cache[key]){
this.cache[key]=[]
}
this.cache[key].push(fn)
}
//arguments 是發(fā)布消息時(shí)候攜帶的參數(shù)數(shù)組
Observer.prototype.emit = function (key) {
if(this.cache[key]&&this.cache[key].length>0){
var fns = this.cache[key]
}
for(let i=0;i<fns.length;i++){
Array.prototype.shift.call(arguments)
fns[i].apply(this,arguments)
}
}
// remove 的時(shí)候需要注意,如果你直接傳入一個(gè)匿名函數(shù)fn,那么你在remove的時(shí)候是無(wú)法找到這個(gè)函數(shù)并且把它移除的,變通方式是傳入一個(gè)
//指向該函數(shù)的指針,而 訂閱的時(shí)候存入的也是這個(gè)指針
Observer.prototype.remove = function (key,fn) {
let fns = this.cache[key]
if(!fns||fns.length===0){
return
}
//如果沒有傳入fn,那么就是取消所有該事件的訂閱
if(!fn){
fns=[]
}else {
fns.forEach((item,index)=>{
if(item===fn){
fns.splice(index,1)
}
})
}
}
//example
var obj = new Observer()
obj.on('hello',function (a,b) {
console.log(a,b)
})
obj.emit('hello',1,2)
//取消訂閱事件的回調(diào)必須是具名函數(shù)
obj.on('test',fn1 =function () {
console.log('fn1')
})
obj.on('test',fn2 = function () {
console.log('fn2')
})
obj.remove('test',fn1)
obj.emit('test')
為什么會(huì)使用發(fā)布訂閱模式呢? 它的優(yōu)點(diǎn)在于:
- 實(shí)現(xiàn)時(shí)間上的解耦(組件,模塊之間的異步通訊)
- 對(duì)象之間的解耦,交由發(fā)布訂閱的對(duì)象管理對(duì)象之間的耦合關(guān)系.