父组件利用事件总线向子组件通信,子组件没有响应是怎么回事

789 阅读1分钟

主题

探讨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的事件发布订阅来实现的,也就是on,emit方法Vue源码on,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的实现,以及装饰者模式