暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

EventBus—事件总线

Geeker工作坊 2020-12-07
454

EventBus 概要

EventBus是消息传递的一种方式,基于一个消息中心,订阅和发布消息的模式。Vue中就有一个消息总线的机制。当然消息总线不仅仅局限于WEB前端,Android 、IOS中的消息中心也是如此实现的。

[1] 设计模式:订阅者发布者模式。

[2] API的设计:

1. 只能构造一个消息对象。

2. on('msgName', func) 订阅消息,msgName:订阅的消息名称 ;func: 订阅的消息,会在执行$emit 后调用。

3. once('msgName', func) 仅订阅一次消息,后订阅的会替换前面订阅的消息。

4. emit('msgName', msg) 发布消息 msgName:消息名称;msg:可选 发布的消息,传递给监听函数的参数。

5. off('msgName') 移除消息。

Vue EventBus 源码解析

VUE中EventBus可以用来进行任何组件之间的通信。EventBus可以当成一个管道,这个管道两端可以接好多组件,两端的任何一个组件都可以进行通信。这个管道就是Vue实例,实例中有四个跟事件派发相关的方法$on $off $emit $once
。API的设计跟上面提到的一样,不过它做了一些增强。

首先总体上介绍一下Vue EventBus的原理:

[1] Vue在实例化的时候会进行很多初始化的操作,其中包括 eventMixin
方法,该方法里包含了上面提及的四个方法。

[2] 四个方法的实现原理如下:

  1. vue实例上会创建一个对象来保存所有要监听的事件:vm._events = {}

  2. 每当我们要监听一个事件,就往vm.events里添加一个键值对,事件的名称作为键,一个空数组作为值。例如我们要监听的事件名称为event1
    ,则vm._events = {event1: []}

  3. 监听事件的回调函数都会添加到对应的数组中,例如我们调用

    vm.$on('event1', cb1);
    vm.$on('event1', cb2);
    vm.$on('event1', cb3);

    则此时vm._events={event1: [cb1, cb2, cb3]}

  4. 当调用移除监听事件的方法时,所做的操作为移除其对应数组里的回调函数,例如当我们调用

    vm.$off('event1', cb1);

    这时cb1就被移除了,vm._events={event1: [cb2, cb3]}

  5. 当我们执行$emit
    触发对应事件时,所做的操作就是把该事件对应数组里的回调函数都拿出来执行一遍,例如当我们调用

    vm.$emit('event1')

    这时候会取出event1
    对应数组里的cb2
    cb3
    执行

  6. $once
    表示该事件只会触发执行一次,后面在触发就没用了,例如当我们调用:

    // 用$on方法监听event2,回调函数为cb4
    vm.$on('event2', cb4);

    // 用$once方法监听event2,回调函数为cb5
    vm.$once('event2', cb5);

    // 触发event2事件,会执行cb4和cb5
    vm.$emit('event2');

    // 再次触发event2事件,这里只会执行cb4,不会执行cb5,cb5只会执行一次
    vm.$emit('event2');

以上即为事件派发方法的基本原理,当然这些方法还有一些稍微复杂一点的使用方式,比如

  • $on(['event1', 'event2'], cb) 监听多个事件

  • $off(['event1', 'event2'], cb) 移除多个事件

  • $off() // 移除事件不传参数

  • $off('event1') // 移除事件传一个参数

  • $off('event1', cb) // 移除事件传两个参数

  • $emit('event1', param1, param2) // 触发事件传参数

看完下面的源码解析就知道这些情况都是怎么处理的了。

    /** Vue EventBus 事件总线 */


    function eventsMixin(Vue) {
    Vue.prototype.$on = function(event, fn) {}
    Vue.prototype.$once = function(event, fn) {}
    Vue.prototype.$off = function(event, fn) {}
    Vue.prototype.$emit = function(event, fn) {}
    }
    Vue.prototype.$on = function(event, fn) {
    const vm = this
    // 我们传入的要监听的事件可能为数组,这时候对数组里的每个事件再递归调用$on方法
    if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
    vm.$on(event[i], fn);
    }
    } else {
    // 之前已经有监听event事件,则将此次监听的回调函数添加到其数组中,否则创建一个新数组并添加fn
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
    }
    Vue.prototype.$emit = function(event) {
    const vm = this;
    const cbs = vm._events[event];
    if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    // $emit方法可以传参,这些参数会在调用回调函数的时候传进去
    const args = toArray(arguments, 1);
    for (let i = 0, l = cbs.length; i < l; i++) {
    try {
    cbs[i].apply(vm, args);
    } catch(e) {
    handleError(e, vm, ("event handler for \"" + event + "\""));
    }
    }
    }
    return vm;
    }
    Vue.prototype.$once = function(event, fn) {
    const vm = this
    // 封装一个高阶函数on,在on里面调用fn
    function on() {
    // 每当执行了一次on,移除event里的on事件,后面再触发event事件就不会再执行on事件了,也就不会执行on里面的fn事件
    vm.$off(event, on);
    // 执行on的时候,执行fn函数
    fn.apply(vm, arguments);
    }
    // 这个赋值是在$off方法里会用到的
    // 比如我们调用了vm.$off(fn)来移除fn回调函数,然而我们在调用$once的时候,实际执行的是vm.$on(event, on)
    // 所以在event的回调函数数组里添加的是on函数,这个时候要移除fn,我们无法在回调函数数组里面找到fn函数移除,只能找到on函数
    // 我们可以通过on.fn === fn来判断这种情况,并在回调函数数组里移除on函数
    on.fn = fn;
    // $once最终调用的是$on,并且回调函数是on
    vm.$on(event, on);
    return vm;
    }
    Vue.prototype.$off = function(event, fn) {
    const vm = this;
    // all
    if (!arguments.length) {
    // 如果没有传参数,则清空所有事件的监听函数
    vm._events = Object.create(null);
    return vm;
    }
    // 如果传的event是数组,则对该数组里的每个事件再递归调用$off方法
    if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
    vm.$off(event[i], fn);
    }
    return vm;
    }
    // 获取当前event里所有的回调函数
    const cbs = vm._events[event]
    // 如果不存在回调函数,则直接返回,因为没有可以移除监听的内容
    if (!cbs) {
    return vm;
    }
    // 如果没有指定要移除的回调函数,则移除该事件下所有的回调函数
    if (!fn) {
    vm._events[event] = null
    return vm;
    }
    // 指定了要移除的回调函数
    let cb;
    let i = cbs.length;
    while (i--) {
    cb = cbs[i];
    // 在事件对应的回调函数数组里面找出要移除的回调函数,并从数组里移除
    if (cb === fn || cb.fn === fn) {
    cbs.splice(i, 1);
    break;
    }
    }
    return vm;
    }

    自己实现一个EventBus

    按照最初的API设计和参考Vue EventBus创建一个自己的EventBus。这个没有支持批量的发布和订阅事件。

      class MyEventBus {
      constructor() {
      // 初始化消息队列 同 _events
      this.msgQueue = {};
      }
      on(event, fn) {
      if (this.msgQueue.hasOwnProperty(event)) {
      if (this.msgQueue[event] === 'function') {
      this.msgQueue[event] = [this.msgQueue[event], fn];
      } else {
      this.msgQueue[event] = [...this.msgQueue[event], fn];
      }
      } else {
      // 初始化的时候不直接使用数组,如果只有一个方法就减少了空间开销
      this.msgQueue[event] = fn;
      }
      }
      // 消息队列中只保存一个消息
      once(event, fn) {
      // 无效检查event是否存在
      this.msgQueue[event] = fn;
      }
      // 发送消息
      emit(event, ...msg) {
      if (!this.msgQueue.hasOwnProperty(event)) return;
      try {
      if (typeof this.msgQueue[event] === 'function') {
      this.msgQueue[event](...msg);
      } else {
      this.msgQueue[event].map(fn = >{
      fn(...msg)
      })
      }
      } catch(e) {
      console.log(e);
      }
      }
      // 移除消息
      off(event) {
      if (!this.msgQueue.hasOwnProperty(event)) return;
      delete this.msgQueue[event]
      }
      }


      /**
      * 使用 EventBus
      * 也可以导出使用
      * const EventBus = new MyEventBus();
      * export default EventBus; 这样保证共享一个实例
      */
      const EventBus = new MyEventBus();
      EventBus.on('event1', (...msg) = >{
      console.log('订阅的消息是', msg)
      });
      EventBus.emit('event1', 'hello world', '123');
      EventBus.off('event1');
      EventBus.emit('event1', 'hello2world');

      小结

      本次分享首先介绍了EventBus原理和API设计,然后介绍了VUE中EventBus实现方式,最后自己实现了一个EventBus。


      文章转载自Geeker工作坊,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

      评论