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.vue
和level2.vue
来看看实际效果
这时有小伙伴可能会问,我明明只点击了第一个按钮,只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>
总结
到这里,想必大家都对mitt
的api
有了一个基本的了解,常用到的基本就是on
和emit
。如果大家还有啥什么不懂的,可以下载案例跑一跑,代码量不多,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库,里面的设计模式、以及一些小的技巧、运算符的运用,都值得我们去学习,在使用中也遇到的一些小坑,都能够让我们的基础更加稳固。
希望看完的朋友可以动动手点个赞再走哟,你们的支持是对我最大的鼓励啊!!!