最近看项目代码的时候发现了 vueuse useEventBus
这个组合函数,就想了解下它是怎么实现的?然后又想到 mitt
是怎么实现的?
首先 mitt 、useEventBus 都是实现了事件的发布订阅,通过阅读 mitt 、useEventBus
的源码,让我了解了它们是如何实现的以及有什么相同点和不同点!
本着好脑子不如烂笔头的思想有了这篇文章!
使用场景
在 Vue 中,当组件嵌套较深时(如 a > b > c > d > e
)这个时候 e 想要直接触发 a 的事件有几种做法:
- 通过
props
逐层传递事件:a 一层一层将事件通过props
传递到 e - 通过
provide
和inject
注入函数:a 通过provide
注入函数,e 通过inject
获取 a 注入的函数进行调用
那反过来呢?
上边的方法只能由上向下传递,无法由下向上传递,即解决了子组件调用父组件函数的问题,无法解决父组件调用子组件函数的问题。
在开发过程中双向事件调用是会存在的,mitt、useEventBus
可以方便简单的解决这类问题!
数据结构 Map、Set
在分析 mitt
和 useEventBus
的实现之前,我们需要了解 js 中的 Map
和 Set
数据结构,因为它们在事件总线的实现中起到了关键作用。
mitt、useEventBus
的事件类型与事件函数的关联是使用 Map
存储的
mitt
的事件函数是使用 Array
进行存储 ,useEventBus
的事件函数则是使用 Set
进行存储
Map
Map
对象保存键值对,并且能够记住键的原始插入顺序。任何值都可以作为 Map
的键或值。
Map 实例方法
set
在 Map 对象尾部添加一个键值对。get
获取 Map 对象的键值对。不存在返回undefined
delete
移除 Map 中与这个值相等的元素,返回一个布尔值。clear()
移除 Map 对象内的所有元素。size
返回 Map 长度。has
返回一个布尔值,表示该值在 Map 中存在与否。entries
、keys
、values
均返回一个可迭代对象。
Map 使用示例
let myMap = new Map();
let keyObj = {};
let keyFunc = function() {};
let keyString = 'a string';
// 添加键
myMap.set(keyString, "和键 'a string' 关联的值");
myMap.set(keyObj, "和键keyObj关联的值");
myMap.set(keyFunc, "和键keyFunc关联的值");
myMap.size; // 3
// 读取值
myMap.get(keyString); // "和键 'a string' 关联的值"
myMap.get(keyObj); // "和键 keyObj 关联的值"
myMap.get(keyFunc); // "和键 keyFunc 关联的值"
myMap.get('a string'); // "和键 'a string' 关联的值"
// 因为 keyString === 'a string'
myMap.get({}); // undefined, 因为 keyObj !== {}
myMap.get(function() {}); // undefined, 因为 keyFunc !== function () {}
Set
Set
对象允许你存储任何类型(无论是还是对象引用)的唯一值。
这里记住两个知识点:Set
可以存储任何类型的值、Set
中的所有数据都是唯一的
Set 实例方法
add
在Set
对象尾部添加一个元素。返回该Set
对象。delete
移除Set
中与这个值相等的元素,返回一个布尔值。clear()
移除Set
对象内的所有元素。size
返回set长度。has
返回一个布尔值,表示该值在Set
中存在与否。entries
、keys
、values
均返回一个可迭代对象。
Set 使用示例
let mySet = new Set();
mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, "some text" ]
let o = {a: 1, b: 2};
mySet.add(o);
mySet.add({a: 1, b: 2}); // o 指向的是不同的对象,所以没问题
mySet.has(1); // true
mySet.has(3); // false
mySet.has(5); // true
mySet.has(Math.sqrt(25)); // true
mySet.has("Some Text".toLowerCase()); // true
mySet.has(o); // true
mySet.size; // 5
mySet.delete(5); // true, 从set中移除5
mySet.has(5); // false, 5已经被移除
mySet.size; // 4, 刚刚移除一个值
// 迭代整个set
// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
for (let item of mySet) console.log(item);
分析 mitt 的实现
mitt 是一个简单的事件发布订阅库,实现了一个简单但功能完整的事件总线
将源码转换为 js 后如下:
首先使用 Map 创建用于存储事件函数的变量,返回了 all、on、off、emit
all 存储所有事件处理函数
all 是存储所有事件处理函数的 Map 实例
你可以在创建 miit 的时候传入自定义的 Map 对象,将其返回可以让用户手动操作这个对象
on 注册事件监听器
on 是用来给指定的类型绑定事件,两个参数:事件类型、事件处理函数
可以通过设置事件类型为 *
来监听所有事件类型的触发
- 首先获取该事件已经绑定的函数
- 如果该事件类型已存在处理函数,则将新处理函数添加到数组中
- 如果该事件类型还没有处理函数,则创建新数组存储事件函数
off 移除事件监听器
off 是用来移除指定类型的绑定事件,两个参数:事件类型、事件处理函数
- 首先获取该事件已经绑定的函数
- 判断指定的类型的事件是否存在
- 判断 “事件处理函数” 是否存在,
- 如果存在则使用
splice
移除indexOf
查找到指定下标的事件函数 5. 其>>> 0
有些魔法的感觉,可以少写indexOf 返回 -1
的处理逻辑- 如果 indexOf 返回 -1(未找到),-1 >>> 0 会转换为 4294967295
- 确保 splice 的第一个参数为非负整数
- 当 handler 不存在时,splice 会尝试从一个很大的索引处删除,实际上不会删除任何元素
- 事件处理函数如果不存在,则清空该事件类型的所有处理函数
emit 触发事件
emit 用于触发指定类型的事件函数,两个参数:事件类型、传递给事件函数的参数
- 首先获取该事件已经绑定的函数
- 事件函数存在,则通过 slice() 创建副本来遍历,从而避免在触发过程中对原数组的修改导致问题
- 获取事件类型是
*
的事件函数 - 通配符
*
的事件函数存在,遍历调用所有事件函数,不同于普通事件函数的调用,通配符*
的事件函数会传递两个参数: 触发的事件类型、事件函数参数
分析 useEventBus 的实现
useEventBus 是一个简单的事件总线的实现类似 vue2 中的 eventBus
将源码转换为 js 后如下:
这里同样是使用了 Map 来存储事件函数
useEventBus
接收一个 key 参数用于绑定触发对应的事件函数,然后返回了 on, once, off, emit, reset
函数
getCurrentScope
getCurrentScope
是 vue3 导出的一个方法
在 useEventBus
执行时通过 getCurrentScope
获取当前的作用域,用于自动清理监听器
on 注册事件监听器
接收一个事件监听器函数作为参数,这里需要注意下 listener
会接收两个可选参数
type EventBusListener<T = unknown, P = any> = (event: T, payload?: P) => void
function on(listener) {
// 获取当前 key 对应的监听器集合,如果不存在则创建新的 Set
const listeners = (events.get(key) || new Set())
// 将接收到的事件监听器函数添加到 Set 集合中
listeners.add(listener)
// 更新 events 中对应 key 的事件监听器函数
events.set(key, listeners)
// 创建取消监听的函数
const _off = () => off(listener)
// 当作用域销毁时,自动取消监听
// 将清理函数添加到 Vue 的作用域清理队列中
scope?.cleanups?.push(_off)
// 返回取消监听的函数
return _off
}
once 注册只触发一次事件监听器,触发后自动解除监听
接收一个事件监听器函数作为参数
function once(listener) {
// 创建一个包装函数,在调用原始监听器之前先解除监听,这样就可以让事件监听函数只触发一次
function _listener(...args) {
// 执行 off 会在移除这个事件监听函数
off(_listener)
// 触发事件监听函数
listener(...args)
}
// 将包装函数传递给 on 进行注册
return on(_listener)
}
off 移除指定的事件监听器
接收一个事件监听器函数作为参数
function off(listener) {
// 获取当前 key 对应的事件监听器集合
const listeners = events.get(key)
// 如果不存在直接返回
if (!listeners)
return
// listeners 的数据结构是 Set,所以可以使用 delete 方法移除对应的事件监听器
listeners.delete(listener)
// listeners 的数据结构是 Set 可以通过 size 方法获取有多少个事件监听器
// 如果没有剩余的监听器,则完全删除该事件总线
if (!listeners.size)
reset()
}
reset 清除事件总线
function reset() {
// events 的数据结构是 Map,可以直接使用 delete 删除对应的 key
// 相当于把一个对象的 字段删除了
events.delete(key)
}
emit 触发事件监听函数
接收两个可选参数:event, payload
会直接传递给事件监听函数
function emit(event, payload) {
// 遍历所有监听器并调用它们,传入接收到的参数
events.get(key)?.forEach(v => v(event, payload))
}
使用示例
mitt
与 useEventBus
的使用有点相似又有点不同
- mitt 可以在所有的 js 运行时中使用,useEventBus 只能在 vue 中使用
- 在 vue 中使用:mitt 在组件卸载时需要手动移除事件监听,useEventBus 会自动处理
- mitt 可以直观的定义一组多个事件且有良好的 ts 类型提示,useEventBus 则更倾向于单个事件的处理
mitt
创建一个 emitter 导出
import mitt from 'mitt'
type EventBus = {
test: {
text: string
status: boolean
}
test1: {
status: string
}
}
export const emitter = mitt<EventBus>()
在需要使用的地方导入然后使用
import { emitter } from './components/mittBus.ts'
// 触发
emitter.emit('test', { text: 'test',status: true })
// 监听
emitter.on('test', (e) => {
console.log(e)
})
emitter.emit('test1', { status: 'test1' })
emitter.on('test1', (e) => {
console.log(e)
})
可以按照功能 or 模块创建多个 emitter 进行使用
useEventBus
export const Test = Symbol('test')
const testBus = useEventBus<string, { text: string } >(Test)
testBus.emit('test', { text: '123' })
testBus.on((type, data) => {
console.log(type, data)
})
export const Test1: EventBusKey<{ status: boolean }> = Symbol('test1')
const test1Bus = useEventBus(Test1)
test1Bus.emit({ status: true })
test1Bus.on((status) => {
console.log(status)
})
都看到这里你学会了吗?