正如咱们所知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
注册的事件