1. 首页
  2. 技术知识

解读Vue3中废弃组件事件的$on,$off 和 $once 实例方法

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

Vue3.0中事件API

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

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

2.x语法

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

eventHub.js

  1. // eventHub.js
  2. const eventHub = new Vue()
  3. export default eventHub

复制代码 ChildComponent.vue

  1. // ChildComponent.vue
  2. import eventHub from './eventHub'
  3. export default {
  4.   mounted() {
  5.     // 添加 eventHub listener
  6.     eventHub.$on('custom-event', () => {
  7.       console.log('Custom event triggered!')
  8.     })
  9.   },
  10.   beforeDestroy() {
  11.     // 移除 eventHub listener
  12.     eventHub.$off('custom-event')
  13.   }
  14. }

复制代码 ParentComponent.vue

  1. // ParentComponent.vue
  2. import eventHub from './eventHub'
  3. export default {
  4.   methods: {
  5.     callGlobalCustomEvent() {
  6.       eventHub.$emit('custom-event') // 如果ChildComponent mounted,控制台中将显示一条消息
  7.     }
  8.   }
  9. }

复制代码 上述代码示例中 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

  1. // ChildComponent.vue
  2. export default {
  3.   mounted() {
  4.     // 添加 eventHub listener
  5.     this.$parent.$on('custom-event', () => {
  6.       console.log('Custom event triggered!')
  7.     })
  8.   },
  9.   beforeDestroy() {
  10.     // 移除 eventHub listener
  11.     this.$parent.$off('custom-event')
  12.   }
  13. }

复制代码 ParentComponent.vue

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

复制代码 父组件使用 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的形式展示如下。

  1. /**
  2. * 向外暴露的默认函数
  3. * @param 入参为 EventHandlerMap 对象 (ts真香,我们能清楚的知道参数的类型是什么,返回值是什么)
  4. * @returns 返回一个对象,对象包含属性 all,方法 on,off,emit
  5. */
  6. export default functiоn mitt (all) {
  7.   /*
  8.     此处实参可传一个EventHandlerMap对象,实现多个 mitt 的合并。例如:
  9.     const m1 = mitt();
  10.     m1.on('hi', () => { console.log('Hi, I am belongs to m1.'); });
  11.     const m2 = mitt(m1.all);
  12.     m2.emit('hi') // Hi, I am belongs to m1.
  13.     m2.on('hello', () => { console.log('Hello, I am belongs to m2.'); });
  14.     m1.emit('hello'); // Hello, I am belongs to m2.
  15.     m1.all === m2.all // true
  16.   */
  17.   all = all || new Map();
  18.   return {
  19.     // 事件键值对映射对象
  20.     all,
  21.     /**
  22.      * 注册一个命名的事件处理
  23.      * @param type 事件名,官方表示事件名如是 *,用来标记为通用事件,调用任何事件,都会触发命名为 * 的事件
  24.      * @param handler 事件处理函数
  25.      */
  26.     on (type, handler) {
  27.       // 根据type去查找事件
  28.       const handlers = all.get(type);
  29.       // 如果找到有相同的事件,则继续添加,Array.prototype.push 返回值为添加后的新长度,
  30.       const added = handlers && handlers.push(handler);
  31.       // 如果已添加了type事件,则不再执行set操作
  32.       if (!added) {
  33.         all.set(type, [handler]); // 注意此处值是数组类型,可以添加多个相同的事件
  34.       }
  35.     },
  36.     /**
  37.      * 移除指定的事件处理
  38.      * @param type 事件名,和第二个参数一起用来移除指定的事件,
  39.      * @param handler 事件处理函数
  40.      */
  41.     off (type, handler) {
  42.       // 根据type去查找事件
  43.       const handlers = all.get(type);
  44.       // 如果找到则进行删除操作
  45.       if (handlers) {
  46.         // 这里用了个骚操作,其实就是找到了,则删除(多个相同的只会删除找到的第一个),没找到则不会对原数组有任何影响
  47.         handlers.splice(handlers.indexOf(handler) >>> 0, 1);
  48.       }
  49.     },
  50.     /**
  51.      * 触发所有 type 事件,如果有type为 * 的事件,则最后会执行。
  52.      * @param type 事件名
  53.      * @param evt 传递给处理函数的参数
  54.      */
  55.     emit (type, evt) {
  56.       // 找到type的事件循环执行
  57.       (all.get(type) || []).slice().map((handler) => { handler(evt); });
  58.       // 然后找到所有为*的事件,循环执行
  59.       (all.get('*') || []).slice().map((handler) => { handler(type, evt); });
  60.     }
  61.   };
  62. }

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

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

  1. // emit 不是单例
  2. const m1 = mitt();
  3. const m2 = mitt();
  4. m1 === m2; // false
  5. // 多个mitt之间可以实现合并,合并后的mitt的all属性指向的是同一个内存地址
  6. const m3 = mitt(m2.all);
  7. m3.all === m2.all; // true
  8. m2.on('hi', () => { console.log('我是m2'); });
  9. m3.emit('hi'); // 我是m2
  10. m2.on('*', () => { console.log('我是公共的'); });
  11. m2.emit('hi');
  12. // 我是m2
  13. // 我是公共的

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

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

原创文章,作者:starterknow,如若转载,请注明出处:https://www.starterknow.com/126925.html

联系我们