正如咱們所知Vue中組件的通訊場景不少, 什麼跨多級父子通訊, 兄弟通訊等等。各類各樣的解決方法網上方法都有以及多得飛起, 我就不復制黏貼, 省點ATP。在Vue生態中已經有很好的方案解決各類疑難雜症的通訊問題, 針對中大型項目, 首選
Vuex
(用過都說好), 可是若是是小型項目使用Vue的eventBus
, 是一個不錯的選擇。咱們接下來簡單說下Vue的事件總線的使用和一些常見的問題。vue
Vue的實例就是一個事件總線:
import Vue from 'vue';
var vm = new Vue({
mounted () {
## 此時就能夠監聽一個自定義事件event1
this.$on('event1', (data) => {
consolo.log(data);
});
},
methods: {
trigger () {
this.$emit('event1', '猴賽雷~');
}
}
});
vm.trigger(); ## 輸出: 猴賽雷~
看得出eventBus的使用就是在一個組件內經過$on一個事件, 在一些操做下$emit事件, 這個看起來跟訂閱發佈是一回事。
可是這裏的eventBus是局部的, 只能在這個組件內用, 若是在一個父組件A監聽了事件, 在B子組件去emit, 是沒有反應的,
由於子組件B本身木有監聽該事件。
<A>
<B></B>
</A>
A.vue文件:
export default {
created () {
this.$on('你是否是嚶嚶怪', (嚶) => {
if (嚶.length >= 3) {
console.log('你就是嚶嚶怪');
}
})
}
}
B.vue文件:
export default {
mounted () {
this.$emit('你是否是嚶嚶怪', '嚶嚶嚶嚶');
}
}
複製代碼
全局的eventBus簡單理解爲在一個文件建立一個新的vue實例而後暴露出去, 使用的時候import這個模塊進來便可。咱們來編寫下這個文件:node
在項目中新增一個文件eventBus.js, 代碼實現以下:
import Vue from 'vue';
const Bus = new Vue();
const eventBus = {
TYPES: { // 'TYPES'
EVENT1: { // 'TYPES.EVENT1'
EDIT: { // 'TYPES.EVENT1.EDIT'
INVOKE: {},
CANCEL: {}, // 'TYPES.EVENT1.EDIT.CANCEL'
CONFIRM: {}
},
ADD: {
INVOKE: {},
CANCEL: {},
CONFIRM: {}
},
},
EVENT2: {
EDIT: {
INVOKE: {},
CANCEL: {},
CONFIRM: {}
},
DELETE: {
INVOKE: {},
CANCEL: {},
CONFIRM: {}
},
}
},
// 註冊事件函數
on (eventType, cb = () => {}) {
Bus.$on(eventType.toString(), (...args) => {
cb(...args);
});
},
// 觸發事件函數
emit (eventType, data) {
Bus.$emit(eventType.toString(), data);
},
// 銷燬註冊事件函數
off (eventType) {
Bus.$off(eventType.toString());
},
// 註冊事件觸發一次後銷燬函數
once (eventType, cb = () => {}) {
Bus.$on(eventType.toString(), (...args) => {
cb(...args);
eventBus.off(eventType.toString());
});
}
};
(function (typeRoot) {
/**
* @param {*} source 要給每一個節點添加鏈的對象
* @param {*} parentNode 當前節點的鏈 好比 EVENT1.EDIT.CANCEL
*/
function addNodeChain(source, parentNode = 'TYPES') {
const isObj = typeof source === 'object';
if (!isObj) return; // 支持傳入默認的字符串方式
const separator = !!parentNode ? '.' : '';
const isObjEmpty = Object.keys(source).length === 0;
if (isObjEmpty) {
source['nodeChain'] = parentNode;
source.toString = function () {
return parentNode;
}
return;
}
for (const key in source) {
if (source.hasOwnProperty(key)) {
source['nodeChain'] = parentNode;
source.toString = function () {
return parentNode;
}
const nodeChain = parentNode + separator + key;
addNodeChain(source[key], nodeChain);
}
}
}
addNodeChain(typeRoot);
Object.freeze(eventBus);
window.eventBus = eventBus;
})(eventBus.TYPES);
export default eventBus;
複製代碼
上面的定義了一個eventBus對象,裏面定義如下五個屬性:git
TYPES (預先定義好的一些事件)
on(監聽(訂閱)事件函數)
emit(觸發(發佈)事件函數
once(只監聽(訂閱)一次事件函數
off(移除事件)
上面定義好的eventBus
對象很好理解,無非就是簡單封裝了下Bus的一些api, 咱們來講說TYPES
對象和addNodeChain
方法。
TYPES
:
由於on方法第一個傳入的參數是字符串, 也是事件名字, 若是訂閱事件多了, 在項目中必然會存在很多的字符串, 若是不當心寫錯事件名(多寫少些個字母啥的~), Vue並不會給你報錯, 這對錯誤跟蹤和定位都是比較困難的, 所以咱們最好有一個專門的文件來管理和維護這些事件名。這裏使用對象嵌套的方式來事先定義事件名稱, 使用的時候好比 eventBus.on(eventBus.TYPES.EVENT1.UPDATE.INVOKE)
, 由於事先沒有定義eventBus.TYPES.EVENT1.UPDATE
, undefined
沒有INVOKE
屬性, 控制檯直接報錯, 這樣作的好處在必定狀況避免了上面所說的問題。github
addNodeChain
:
這個方法是遍歷整個對象, 給每一個節點添加nodeChain
屬性, 也是當前節點的到根節點的鏈。 而且重寫了當前節點的toString方法, 返回當前節點的nodeChain
, 咱們來打印一下執行addNodeChain
後eventBus.TYPES的結構。api
{EVENT1: {…}, EVENT2: {…}, nodeChain: "TYPES", toString: ƒ}
EVENT1: {
ADD: {
CANCEL: {
nodeChain: "TYPES.EVENT1.ADD.CANCEL",
toString: ƒ (),
},
CONFIRM: {nodeChain: "TYPES.EVENT1.ADD.CONFIRM", toString: ƒ},
INVOKE: {nodeChain: "TYPES.EVENT1.ADD.INVOKE", toString: ƒ},
nodeChain: "TYPES.EVENT1.ADD",
toString: ƒ (),
},
EDIT: {INVOKE: {…}, CANCEL: {…}, CONFIRM: {…}, nodeChain: "TYPES.EVENT1.EDIT", toString: ƒ}
nodeChain: "TYPES.EVENT1",
toString: ƒ (),
},
EVENT2: {EDIT: {…}, ADD: {…}, nodeChain: "TYPES.EVENT2", toString: ƒ}
nodeChain: "TYPES",
toString: ƒ (),
__proto__: Object,
而在定義事件方法on時:
on (eventType, cb = () => {}) {
Bus.$on(eventType.toString(), (...args) => {
cb(...args);
});
},
上面會把接受進來的參數調用toString方法,傳入給Bus.$on, 所以事件名稱仍是字符串。
至此, 咱們的eventBus就能夠用了。
複製代碼
咱們驗證常見的兩個場景:bash
有如下父組件A, 和子組件B, C, 其中B, C是兄弟組件:
A.vue:
<template>
<div>
<h1>這個是父組件</h1>
<B></B>
<C></C>
</div>
</template>
<script>
import B from './B';
import C from './C';
import eventBus from '@/util/eventBus.js';
export default {
created () {
this.eventBus.on(this.eventBus.TYPES.EVENT1.ADD.CONFIRM, (data) => {
console.log(data);
})
},
components: {
B,
C,
}
}
</script>
--------------------------------------------------------------------------------------------
B.vue:
<template>
<div>
<h3>這個是子組件B</h3>
<button @click="emitParentEvent">點擊觸發父組件A事件</button>
<button @click="emitBrotherEvent">點擊觸發兄弟組件C事件</button>
</div>
</template>
<script>
import eventBus from '@/util/eventBus.js';
export default {
methods: {
emitParentEvent () {
eventBus.emit(eventBus.TYPES.EVENT1.ADD.CONFIRM, '父組件A事件被觸發了');
},
emitBrotherEvent () {
eventBus.emit(eventBus.TYPES.EVENT1.ADD.INVOKE, '兄弟組件C事件被觸發了');
}
}
}
</script>
--------------------------------------------------------------------------------------------
C.vue:
<template>
<div>
<h3>這個是子組件C</h3>
</div>
</template>
<script>
import eventBus from '@/util/eventBus.js';
export default {
created () {
eventBus.on(eventBus.TYPES.EVENT1.ADD.INVOKE, (data) => {
console.log(data);
})
},
}
</script>
複製代碼
咱們在父組件A和子組件C都註冊了事件, 在B組件中經過點擊對應的按鈕emit事件, 當點擊按鈕時控制檯輸出:函數
嗯, 預期符合結果~~~當咱們來回點擊上面切換路由, 從新渲染 (父組件A會從新render), 而後再去點擊B組件的按鈕emit事件, 你會發現, 事件會被重複執行屢次, 好比我切換了6次, 事件被觸發了6次, emmm:ui
緣由是由於在父組件A和子組件C被destory時候, eventBus.$on的事件是不會被銷燬, 組件的每次從新render, 事件就會疊加註冊, 而eventBus是全局的,它不會隨着你頁面切換而從新執行生命週期。
issue: github.com/vuejs/vue/i…
this
尤大對這個問題也做出瞭解析, 如圖:spa
既然eventBus
不會隨着組件的銷燬而註銷事件, 那咱們能夠主動去註銷掉事件, 具體的方法就是在eventBus.on
的組件中,在beforeDestroy
或者 destoryed
生命週期中off
事件, 咱們來修改一下A.vue和C.vue;
A.vue:
export default {
beforeDestroy () {
eventBus.off(eventBus.TYPES.EVENT1.ADD.CONFIRM);
}
}
-------------------------------------------------------------------------------------
C.vue:
export default {
beforeDestroy () {
eventBus.off(eventBus.TYPES.EVENT1.ADD.INVOKE);
}
}
</script>
複製代碼
修改後, 就不會出現渲染組件, 疊加註冊事件的bug, 每次點擊按鈕只會被emit一次。
尤大建議到可使用
mixin
注入來處理這個bug, 在mixin
組件的鉤子中off
註冊的事件