🍖 vue3.0系列-mitt迷你库源码解析

3,687 阅读4分钟

Mitt是一个微型的 EventEmitter 库,在Vue3中,官方推荐使用它替代已经移除的EventBus

小猪配淇手上纹,我们都是打工人🤣。

这次我们来研究一下mitt这个小而美的工具库,接我之前的一篇文章Vue3跨组间通信。之所以说它小而美,是因为他的源码仅不到20行,压缩后2少于200字节,而我们却能从里面学习到设计模式、和一些小技巧,能够很好的填补我们的基础。 接下来,让我们从实例出发,进入mitt的小世界吧!

代码基本上每一行都有注释,期望每一位小伙伴都能看懂。当然,作者本身也只是一名初级前端,其中要是有啥讲的不对的,也请大佬们指出,拜谢🙈。

初始化环境

项目版本

  • 脚手架:@vue/cli 4.5.6
  • vue: ^3.0.0
  • mitt:^2.1.0

项目结构

需要关注的文件

-src
--components
---level2.vue  子组件
---level3.vue  曾子组件
--assets
---mitt.js mitt源码
--views
---level1.vue  父组件
--main.js

下载案例

git clone https://github.com/taosiqi/vue3-mitt.git
cd vue3-mitt
yarn 
yarn serve

案例分析

mian.js

import mitt from './assets/mitt' //引入mitt库
const emitter =new mitt(); //实例化mitt
const app = createApp(App);
app.config.globalProperties.$emitter = emitter; //挂载到全局
app.use(store).use(router).mount('#app')

on && emit

首先,我们看到level3.vue文件。 我们通过getCurrentInstance拿到组件实例,继而通过proxy,获取当前上下文。 我们使用proxy.$emitter.on订阅一个事件,它接收两个参数,第一个是订阅的事件名,第二个是回调函数。事件名也分为两种,一种是订阅一个具体事件,另一种是订阅所有事件,代码中有体现到(28行)。

<template>
  <div class="level3">
    <div>level3</div>
    <button>str1值:{{ str1 }}</button>
    <button>str2值:{{ str2 }}</button>
    <button>str3值:{{ str3 }}</button>
  </div>
</template>

<script>
import {getCurrentInstance, ref} from 'vue'; //获取组件实例
export default {
  name: 'level3',
  setup() {
    const {proxy} = getCurrentInstance();
    let str1 = ref(1)
    let str2 = ref(1)
    let str3 = ref(1)
    function str3Fn(e) {
      str1.value++
    }
    //on方法用于订阅一个事件,第一个参数是事件名称,第二个参数是回调函数,他的回调函数接收一个参数,为传递过来的参数。
    proxy.$emitter.on('str1', str3Fn);
    proxy.$emitter.on('str2', e => {
      str2.value++
    });
    //on(*),订阅所有事件,任何一个emit都会触发它的执行,他的回调函数接收两个参数,第一个参数为发布事件名,第二个才是传递过来的参数。
    proxy.$emitter.on('*', (e,data) => {
      console.log(e,data)
      str3.value++
    });
    proxy.$emitter.on('clearStr1', e => {
      // off用于退订。这里需要注意,工具内部使用indexOf判断,第二个参数不要传入匿名函数,否则会出现退订失败。
      proxy.$emitter.off('str1', str3Fn)
    });
    return {
      str1,
      str2,
      str3,
    };
  },
}
</script>

再看下level1.vue文件。 我们可以使用proxy.$emitter.emit发布一个事件,它接收两个参数,第一个是订阅的事件名,第二个是需要传递的参数(23行)。

<template>
  <div class="level1">
    <div>level1</div>
    <button @click="clickOneFn">改变level3->str1</button>
    <button @click="clickTwoFn">改变level3->str2</button>
  </div>
  <level2 msg="level2"/>
</template>

<script>
import level2 from '@/components/level2.vue'
import {getCurrentInstance} from 'vue'; //获取组件实例
export default {
  name: 'level1',
  components: {
    level2
  },
  setup() {
    const {proxy} = getCurrentInstance();

    function clickOneFn() {
      // emit用来发布一个事件,第一个参数是订阅名称,第二个参数是需要传递的参数
      proxy.$emitter.emit('str1', {data: '改变str1'});
    }

    function clickTwoFn() {
      proxy.$emitter.emit('str2', {data: '改变str2'});
    }

    return {clickOneFn, clickTwoFn};
  }
}
</script>

到这里,我们可以结合level1.vuelevel2.vue来看看实际效果 image.png 这时有小伙伴可能会问,我明明只点击了第一个按钮,只emit了str1,那为什么我们level3里面的str3变量也发生了改变捏。导致这种情况出现的原因就是,我们还使用了on('*'),他会响应任何一个emit,不管他是emit('str1')还是emit('str2'),都会导致它执行一次。

proxy.$emitter.on('*', (e,data) => {
      console.log(e,data)
      str3.value++
});

off && clear

接下来我们需要关注到lever2以及lever3两个文件,$emitter.all.clear移除所有订阅者,$emitter.off用于移除单个订阅者。这里需要注意的是,off内部使用indexOf判断回调函数是否存在,存在则移除。所有我们如果有移除的需求,第二个参数就不要传入匿名函数了,否则会出现退订失败的情况。

//level2
<template>
  <div class="level2">
    <div>level2</div>
    <button @click="clearAllFn">使用clear清除所有</button>
    <button @click="clearOneFn">使用off清除单个</button>
  </div>
  <level3 msg='level3'></level3>
</template>

<script>
import { getCurrentInstance } from 'vue'; //获取组件实例
import level3 from '@/components/level3.vue'
export default {
  name: 'level2',
   components: {
    level3
  },
  setup() {
    const { proxy } = getCurrentInstance();
    // 退订所有,工具内部使用Map管理订阅者,clear实际是map自带的方法,用于移除Map的所有键值对
    function clearAllFn(){
      proxy.$emitter.all.clear()
    }
    // 退订指定订阅者,具体退订代码看level3文件。
    function clearOneFn(){
      proxy.$emitter.emit('clearStr1');
    }
    return {
      clearAllFn,clearOneFn
    };
  },
}
</script>
//level3部分代码
<script>
import {getCurrentInstance, ref} from 'vue'; //获取组件实例
export default {
  name: 'level3',
  setup() {
    const {proxy} = getCurrentInstance();
    let str1 = ref(1)
    function str3Fn(e) {
      str1.value++
    }
    proxy.$emitter.on('clearStr1', e => {
      // off用于退订。这里需要注意,工具内部使用indexOf判断,第二个参数不要传入匿名函数,否则会出现退订失败。
      proxy.$emitter.off('str1', str3Fn)
    });
    return {
      str1,
    };
  },
}
</script>

总结

到这里,想必大家都对mittapi有了一个基本的了解,常用到的基本就是onemit。如果大家还有啥什么不懂的,可以下载案例跑一跑,代码量不多,api也简单,相信大家都可以看懂的😁😁。下来开始我们的源码解析,hah。

源码解析

源码

mitt.js是我从官方库提取出来的源码,有没有发现代码很少,不到20行。

module.exports = function (n) {
  //发布订阅模式
  return {
    /**
     * 实例化一个Map结构的n,用于管理订阅者
     */
    all: (n = n || new Map()),
    /**
     * on方法用于订阅一个事件
     * @param e (string | symbol) 事件名
     * @param t Function  回调方法
     */
    on: function (e, t) {
      var i = n.get(e); //get方法用于返回键对应的值,如果不存在,则返回undefined。
      /**
       * 分两种情况
       * if i为undefined,则(i && i.push(t))为false, 执行 || 右边,往n里面设置键值对。
       * else i不为undefined,则往i里面push传入的t方法。这也是为啥项目热更新后,点击emit会执行多次的原因
       */
      (i && i.push(t)) || n.set(e, [t]);
    },
    /**
     * off退订指定订阅者
     * @param e  (string | symbol) 事件名
     * @param t   Function 要删除的回调函数,这里是对比回调函数而不是key,所有务必传递正确
     */
    off: function (e, t) {
      var i = n.get(e);
      /**
       * 分两种情况
       * if i为undefined,则不处理(传入了不存在的订阅者)。
       * else 使用splice达到删除功能。这里巧用了无符号右移运算符,我们知道indexOf会返回指定元素出现的位置,不存在则会返回-1,
       * 当为-1时,(i.indexOf(t) >>> 0)===4294967295,splice自然无法截取到,这样省去了if else判断。
       * 注意的是,第二个参数不要使用匿名函数(箭头函数),两个匿名函数不是同一个内存地址,indexOf是强等于判断,会导致退订失败。
       * 无符号右移运算符->https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Unsigned_right_shift
       */
      i && i.splice(i.indexOf(t) >>> 0, 1);
    },
    /**
     * emit用来发布一个事件
     * @param e  (string | symbol) 事件名
     * @param t   Any 传递的参数
     */
    emit: function (e, t) {
      /**
       * 发布事件,需要处理两种情况,1:发布指定名称为e的事件 2:发布(*)事件。他们传递的参数不一样
       * (n.get(e) || []) get返回键对应的值,如果为undefined,则返回一个数组,防止slice报错。
       * slice返回一个新数组,原数组不会改变,slice有两个参数,都是可选的(有些文档说第一个参数必填,咋们以mdn为准),
       * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice 。
       * map循环方法,并把参数t传递过去
       */
      (n.get(e) || []).slice().map(function (n) {
        n(t);
      }),
        (n.get("*") || []).slice().map(function (n) {
          n(e, t);
        });
    },
  };
};

总结

小而美的mitt库,里面的设计模式、以及一些小的技巧、运算符的运用,都值得我们去学习,在使用中也遇到的一些小坑,都能够让我们的基础更加稳固。
希望看完的朋友可以动动手点个赞再走哟,你们的支持是对我最大的鼓励啊!!!

引用

  1. Array.slice
  2. >>>运算符
  3. mitt官方库 更改