主题
探讨vue的事件发布订阅,组件间通信eventBus,以及产生的一个bug,明明监听了为啥不触发
问题
今天有个同事问我,打算通过eventBus的方式,从父组件向子组件传值,代码如下:
// father
export default {
template: '<div class="father"><h1>father</h1><div ><button @click="sendToChild">发消息给儿子</button></div><child></child></div>',
components: {
child,
},
data() {
return {
};
},
methods: {
sendToChild() {
Bus.$emit('fromFather', 'hello son by click');
},
},
created() {
Bus.$emit('fromFather', 'hello son');
}
}
// child
const defalut {
template: '<div class="child"><strong>fromFather:</strong>{{info}}</div>',
data() {
return {
info: '--',
};
},
mounted() {
Bus.$on('fromFather', (info) => {
this.info = info;
});
}
}
他问我,为啥子组件没有响应父组件传过来的事件呢?明明子组件是订阅了事件啊
分析
首先看看订阅事件和发布事件都在哪?父组件发布的事件写在created生命周期阶段,子组件订阅写在mounted生命周期阶段。
了解vue生命周期肯定知道,父组件的created在子组件的mounted之前运行,也就是说父组件在发布事件的时候,子组件还没有生成。可以猜测是因为父组件在发布事件的时候子组件还没有订阅,所以子组件自然就无法响应。
先来看看eventBus是怎么实现的?代码如下:
//eventBus.js
import Vue from 'vue';
import '../../public/utils';
const eventBus = new Vue();
export default eventBus;
实例化一个vue对象,利用vue的事件发布订阅来实现的,也就是emit方法Vue源码
emit源码
从源码可以看出,vue的事件发布订阅是不支持先发布后订阅再来响应事件的。
那怎样解决这个问题呢?
----------思考中间线--------------
解决
后来同事用vuex状态管理器解决了,那如果不用怎么解决?
其实现在问题变为了怎样做能让vue支持事件先发布再订阅。
解决问题的方法是在触发事件的时候,以事件名为key值,缓存起来。当后面监听这个事件的时候再判断一下这个事件名之前有没有触发,如果有,回调函数就传入缓存的值执行一遍。
伪代码就是
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
// ... vue的源码
if(以事件名event为key的对象有值) {
fn(缓存值)
}
}
Vue.prototype.$emit = function (event: string | Array<string>, fn: Function): Component {
// ... vue的源码
// 默认以事件名缓存值
}
有人就说了,你这是叫我重写vue源码?这肯定不行。那有没有什么办法在不重写源码的情况下给方法添加额外的功能。也就是做到先执行完vue里面的事件方法,再执行自己的方法。
我想到的是装饰者模式。(装饰着模式是什么,怎么实现看这里)
// util.js
Function.prototype.before = function(beforeFunc){
var that = this;
return function(){
beforeFunc.apply(this, arguments);
return that.apply(this, arguments);
}
}
Function.prototype.after = function(afterFunc){
var that = this;
return function(){
var ret = that.apply(this, arguments);
afterFunc.apply(this, arguments);
return ret;
}
}
然后在eventBus里面添加代码,为事件添加额外功能
// eventBus.js
import Vue from 'vue';
import './utls.js';
const eventBus = new Vue();
eventBus._cacheTurbo = [];
Vue.prototype.$emit = Vue.prototype.$emit.after(function cacheEvents(type, info) {
//默认缓存
this._cacheTurbo[type] = info;
});
Vue.prototype.$on = Vue.prototype.$on.after(function cacheEvents(type, cb) {
if (this._cacheTurbo[type]) {
//说明有缓存的 可以执行
cb(this._cacheTurbo[type]);
}
});
export default events;
好了,现在这个问题解决了。当然我不支持这种方法,因为始终都覆盖了vue的源码。只想通过这个问题让大家了解事件的发布订阅模式,eventBus的实现,以及装饰者模式