本文要讲解的内容原文档地址点我传送

Vue3.0中事件API

$on$off$once 实例方法已被移除,应用实例不再实现事件触发接口。

接下来我们分析下为什么要在Vue3中去掉,如果需要继续使用此功能改为使用第三方 mitt 库替代。

2.x语法

在 2.x 中,Vue 实例可用于触发通过事件触发 API 强制附加的处理程序 ($on$off$once),这用于创建 event hub,以创建在整个应用程序中使用的全局事件侦听器:

eventHub.js

// eventHub.js

const eventHub = new Vue()

export default eventHub

ChildComponent.vue

// ChildComponent.vue
import eventHub from './eventHub'

export default {
  mounted() {
    // 添加 eventHub listener
    eventHub.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // 移除 eventHub listener
    eventHub.$off('custom-event')
  }
}

ParentComponent.vue

// ParentComponent.vue
import eventHub from './eventHub'

export default {
  methods: {
    callGlobalCustomEvent() {
      eventHub.$emit('custom-event') // 如果ChildComponent mounted,控制台中将显示一条消息
    }
  }
}

上述代码示例中 ChildComponent.vue 和 ParentComponent.vue 都引用了同一个 Vue 的实例对象 eventHub。使用 eventHub 的目的就是在调用 $on, $off, $emit 时是同一个对象上的方法调用。

我们知道组件中的 this 也就是一个 Vue 的实例对象。

说到这里,善于思考的同学可能会想到,我们是不是可以将上述的 eventHub.xx 改为 this.xx
答案是不可以的,因为如此一来 ChildComponent.vue 中的 this 和 ParentComponent.vue 的 this 将不会是同一个 Vue 实例对象,也就是说 (new ChildComponent) !== (new ParentComponent)

上述代码实际可以改为如下

ChildComponent.vue

// ChildComponent.vue
export default {
  mounted() {
    // 添加 eventHub listener
    this.$parent.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // 移除 eventHub listener
    this.$parent.$off('custom-event')
  }
}

ParentComponent.vue

// ParentComponent.vue
export default {
  methods: {
    callGlobalCustomEvent() {
      this.$emit('custom-event') // 如果ChildComponent mounted,控制台中将显示一条消息
    }
  }
}

父组件使用 this.$emit 去触发事件,子组件中使用 this.$parent 去获取父组件的实例对象。但如果是多层级的子孙组件,这就比较难受了,需要 this.$parent.$parent...

所以在Vue2.x中 this.$on this.$off this.$emit 通常只是在一个组件内进行使用,父子组件使用的时候要考虑实例对象是否是同一个。而在一个组件内的时候,我们可以直接this.来调用其他方法,不需要这么麻烦。因此将这几个方法从Vue3.0中移除。$emit 仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序

Vue3.x推荐使用外部库mitt来代替 $on $emit $off

接下来部分是解读 mitt 源码

mitt源码使用的是typescript编写的,源码加注释一共不到90行,阅读起来比较轻松。typescript不是本次的重点,所以我将mitt源码以js的形式展示如下。

/**
 * 向外暴露的默认函数
 * @param 入参为 EventHandlerMap 对象 (ts真香,我们能清楚的知道参数的类型是什么,返回值是什么)
 * @returns 返回一个对象,对象包含属性 all,方法 on,off,emit
 */
export default function mitt (all) {
  /*
    此处实参可传一个EventHandlerMap对象,实现多个 mitt 的合并。例如:
    const m1 = mitt();
    m1.on('hi', () => { console.log('Hi, I am belongs to m1.'); });

    const m2 = mitt(m1.all);
    m2.emit('hi') // Hi, I am belongs to m1.
    m2.on('hello', () => { console.log('Hello, I am belongs to m2.'); });

    m1.emit('hello'); // Hello, I am belongs to m2.

    m1.all === m2.all // true
  */
  all = all || new Map();

  return {
    // 事件键值对映射对象
    all,

    /**
     * 注册一个命名的事件处理
     * @param type 事件名,官方表示事件名如是 *,用来标记为通用事件,调用任何事件,都会触发命名为 * 的事件
     * @param handler 事件处理函数
     */
    on (type, handler) {
      // 根据type去查找事件
      const handlers = all.get(type);
      // 如果找到有相同的事件,则继续添加,Array.prototype.push 返回值为添加后的新长度,
      const added = handlers && handlers.push(handler);
      // 如果已添加了type事件,则不再执行set操作
      if (!added) {
        all.set(type, [handler]); // 注意此处值是数组类型,可以添加多个相同的事件
      }
    },

    /**
     * 移除指定的事件处理
     * @param type 事件名,和第二个参数一起用来移除指定的事件,
     * @param handler 事件处理函数
     */
    off (type, handler) {
      // 根据type去查找事件
      const handlers = all.get(type);
      // 如果找到则进行删除操作
      if (handlers) {
        // 这里用了个骚操作,其实就是找到了,则删除(多个相同的只会删除找到的第一个),没找到则不会对原数组有任何影响
        handlers.splice(handlers.indexOf(handler) >>> 0, 1);
      }
    },

    /**
     * 触发所有 type 事件,如果有type为 * 的事件,则最后会执行。
     * @param type 事件名
     * @param evt 传递给处理函数的参数
     */
    emit (type, evt) {
      // 找到type的事件循环执行
      (all.get(type) || []).slice().map((handler) => { handler(evt); });
      // 然后找到所有为*的事件,循环执行
      (all.get('*') || []).slice().map((handler) => { handler(type, evt); });
    }
  };
}

代码还是相当的精简的,麻雀虽小,五脏俱全。

接下来我们写几个例子来小牛试刀。

// emit 不是单例
const m1 = mitt();
const m2 = mitt();
m1 === m2; // false

// 多个mitt之间可以实现合并,合并后的mitt的all属性指向的是同一个内存地址
const m3 = mitt(m2.all);
m3.all === m2.all; // true

m2.on('hi', () => { console.log('我是m2'); });
m3.emit('hi'); // 我是m2

m2.on('*', () => { console.log('我是公共的'); });
m2.emit('hi');
// 我是m2
// 我是公共的

到此我们已经学会如果使用 emitt 以及如何使用它了。

那么这里留个作业给大家发挥下,我们如何基于它实现一个单例的 mitt 呢?

Logo

前往低代码交流专区

更多推荐