前言
在Vue3 Composition API中$on
、$off
和$once
实例方法已经被移除,组件实例不再实现事件触发接口,导致EventBus
无法使用。
很多人议论取消是对的,太多的emit
、on
在大型项目中兼职都是噩梦,不易管理,调试起来还很麻烦等等问题。
个人觉得仁者见仁,存在即有它的意义,主要看项目需求、技术需求需不需要使用。在我们前端代码规范里明确规定,禁止使用事件总线,原因是因为我们项目代码量太大,使用起来不好维护,出现跨组件通信更多的是使用Vuex
。如果需要使用,就做好取消订阅、事件名管理、统一封装使用等操作,便于后续的管理。
回归正题,由于Vue3
中取消了,有没有什么办法可以继续使用?
其实都知道,EventBus
就是一个事件总线,Vue3
中没有提供,就找一个替代品提供对应功能就行。
这里主要介绍的是Mitt,它是一个轻量级的JavaScript
事件总线库,大小只有200b
,提供了基本的事件处理功能,包括事件的订阅、取消订阅和触发,还支持使用命名空间来进行更高级的事件处理。
对事件总线不是很了解的,后面来手动实现一个事件总线,这在面试中也经常会问到,是一道经典面试题。
Mitt
安装
使用npm
、cnpm
、yarn
等管理安装。
bash
npm install --save mitt
在Vue
中引入并初始化并全局挂载,然后在任何地方都可以使用。
ts
// main.ts
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import mitt from 'mitt'
const app = createApp(App)
// 全局属性上扩展声明一下 $bus
const Mit = mitt()
declare module 'vue' {
export interface ComponentCustomProperties {
$bus: typeof Mit
}
}
// 全局挂载
app.config.globalProperties.$bus = Mit
app.use(createPinia())
app.use(router)
app.mount('#app')
顺便扩展全局属性上的$bus
属性声明,这样在开发时有更好的提示。
使用
由于是挂载到全局属性上,在setup
语法里不能通过this.$bus
的方式去访问,需要使用getCurrentInstance
获取当前实例。
ts
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
instance?.appContext.config.globalProperties.$bus.emit('emitName', 'sendValue')
如果每次使用都需要写这么多就很麻烦,这时候可以通过Hook
的方式对代码进行封装。
在根目录新建一个hooks/userGetGlobalProperties.ts
文件,用来获取全局上的属性。
ts
// userGetGlobalProperties.ts
import { getCurrentInstance } from 'vue'
export default function userGetGlobalProperties() {
const instance = getCurrentInstance()
const globalProperties = instance?.appContext.config.globalProperties
return { ...globalProperties }
}
在使用的时候直接调用userGetGlobalProperties hook
就行
ts
import userGetGlobalProperties from './hooks/userBus'
const { $bus } = userGetGlobalProperties()
$bus?.emit('emitName', 'sendValue')
在组件上使用
它的一个作用就是用来解决组件间嵌套、关系比较复杂时,组件如何进行通信的问题,这里就用兄弟组件来举例。
兄弟组件A
html
<template>
<span>我是兄弟组件A,我要向兄弟组件B</span>
<button type="button" @click="send">发送</button>
</template>
<script setup lang="ts">
import userGetGlobalProperties from '../hooks/userBus'
const { $bus } = userGetGlobalProperties()
function send() {
$bus?.emit('onSendName', 'inkun')
}
</script>
兄弟组件B
html
<template>
<span>我是兄弟组件B,我会接收我兄弟</span>
<span>我兄弟是{{ userName }}</span>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import userGetGlobalProperties from '../hooks/userBus'
let userName = ref('')
const { $bus } = userGetGlobalProperties()
$bus?.on('onSendName', (value) => {
userName.value = value as string
console.log('value', value)
})
</script>
在一个组件里面使用
html
<template>
<div class="container">
<span>app组件</span>
<MyComponentA></MyComponentA>
<br />
<MyComponentB></MyComponentB>
</div>
</template>
<script setup lang="ts">
import MyComponentB from './components/MyComponentB.vue'
import MyComponentA from './components/MyComponentA.vue'
</script>
API
emit 派发事件
接收两个参数,第一个参数为事件名,第二个参数传递的参数
ts
$bus?.emit('emitName', 'sendValue')
on 监听事件
接收两个参数,第一个参数为事件名,第二参数为回调函数,参数是emit
中传入的参数。
ts
$bus?.on('emitName', (value) => {
console.log(value)
})
on
还可以监听所有事件,在事件名出用*
,代表监听所有事件
ts
$bus?.on('*', (value) => {
console.log(value)
})
off 移除事件
移除某个事件,第一个参数为事件名,第二个参数为on
时执行的函数。
ts
const Fn = (value: any) => {
console.log(value)
}
$bus?.off('emitName', Fn)
clear 清空所有事件
清除所有监听的事件
ts
$bus?.all.clear()
为miit标注类型
为什么要标注类型?看过上面ikun
那个例子就知道,我在兄弟组件B接受数据时并赋值给name
时,使用了as
断言,如果不知道的可以看看我以往的TypeScript #类型断言(Type Assertion)里面有相关介绍。
为什么要断言,因为接收到的值数据类型是unknown
怎么解决呢?一种是在接收的时候进行断言处理,另一种就是为mitt
进行类型标注,直接定义类型,这样在接收的时候就不会报错。
diff
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import mitt from 'mitt'
const app = createApp(App)
+ type Events = {
+ onSendName: string
+ }
- const Mit = mitt()
+ const Mit = mitt<Events>()
declare module 'vue' {
export interface ComponentCustomProperties {
$bus: typeof Mit
}
}
// 全局挂载
app.config.globalProperties.$bus = Mit
app.use(createPinia())
app.use(router)
app.mount('#app')
这样做的好处就是,既可以为on
标注了接收的数据类型,还可以管理所有的emit
,写上注释。
为了尽量保持main.ts
功能单一,吧mitt
单独抽离出来。当然,也可以为类型标注单独建立一个mitt.d.ts
文件管理,看个人。
ts
// mitt.ts
import mitt from 'mitt'
type Events = {
onSendName: string
...
}
const Mit = mitt<Events>()
declare module 'vue' {
export interface ComponentCustomProperties {
$bus: typeof Mit
}
}
export default Mit
在mian.ts
中引入即可
ts
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import Mit from './utils/mitt'
const app = createApp(App)
// 全局挂载
app.config.globalProperties.$bus = Mit
app.use(createPinia())
app.use(router)
app.mount('#app')
手写EventBus
在此之前,先了解一下什么是事件总线。
什么是事件总线
事件总线是一种软件架构模式,用于在分布式系统中实现组件间的通信和信息传递。它提供了一种解耦的方式,使得系统中的各个组件可以通过发送和接收事件来进行通信,而不需要直接依赖于彼此的具体实现。
在事件总线模式中,系统中的组件可以分为两类:事件生产者 和事件消费者。事件生产者负责生成事件并将其发布到事件总线上,而事件消费者订阅感兴趣的事件并在事件发生时进行相应的处理。
而在EvnetBus
中,通常使用emit(或publish) 和 on(或subscribe) 来表示事件的生产者和消费者。
- emit(或publish) 是用于表示事件的生产者。当某个组件或模块产生一个事件时,它会将该事件发布到事件总线上,以通知其他组件。
- on(或subscribe) 则表示事件的消费者。当组件或模块对某个事件感兴趣时,它会订阅该事件,以便在事件发生时接收并处理相应的通知。
生产者使用emit
将事件发布到事件总线 上,而消费者使用on
订阅感兴趣的事件。当生产者发布事件时,事件总线会将事件传递给所有订阅了该事件的消费者,消费者可以根据需要对事件进行处理。
原理
事件总线的原理就是运用了JS设计模式之布-订阅模式。通过发布-订阅模式,可以让生产者将事件发布到事件总线上,而其他组件(消费者)可以自行订阅事件总线上的事件,并在时间发生时接收和处理这些事件。
基本工作原理
- 创建事件总线对象:首先,创建一个事件总线对象,它将用于管理事件的发布和订阅。
- 注册事件订阅者 :组件可以通过订阅方法(如
on
、subscribe
等)将自己注册为感兴趣的事件订阅者。订阅者提供一个回调函数,用于处理事件发生时的逻辑。 - 发布事件 :当某个事件发生时,事件发布者使用事件总线对象的发布方法(如
emit
、publish
等)发布事件。事件对象包含事件类型和相关数据。 - 事件传递:事件总线接收到发布的事件后,会将事件传递给所有订阅了该事件类型的订阅者。这通常是通过调用订阅者提供的回调函数来实现。
- 事件处理:订阅者收到事件后,执行回调函数进行事件处理。它们可以访问事件对象中的数据,并根据需要执行逻辑操作。
在面试中,经常会被问到这个问题,问了必定会考手写发布-订阅模式
代码
代码理解起来不难,主要就是几个功能函数。
- on :用来监听收集事件,当传入
key
后先从eventList
中查看有没有注册,如果有注册就将回调函数以数组的形式,放入eventList
存储起来,方便后面使用。- 之所以使用数组存储,是为了解决多个订阅时且处理函数不一样,一个生产者是可以对应多个消费者。
- emit :用来发布事件,触发
on
里的回调函数。先从eventList
中取出对应事件,由于存入的数据是一个数组,遍历循环并触发所有的回调函数就行。 - off :用来取消监听某个事件,在
on
那里是将整个回调函数以数组集合的形式存储起来,所以在删除的时候就需要先找到具体要取消的那个callback
。通过indexOf
先找到函数,然后删除就行。
其实这就是mitt的源码,发现它很巧妙,通过Map
数据结构来存储,然后将Map
当作all
返回,所以我们可以通过$bus.all.clear
删除Map
里的所有数据,还可以使用Map
的一些其他方法。
ts
function bus() {
// 事件中心,用来存储所有事件
const eventList = new Map()
return {
/**
* 订阅者
*
* @param {string} key
* @param {function} callback
*/
on(key, callback) {
const handlers = eventList.get(key)
if (handlers) {
handlers.push(callback)
} else {
eventList.set(key, [callback])
}
},
/**
* 发布事件
*
* @param {string} key
* @param args
*/
emit(key, ...args) {
const handlers = eventList.get(key)
if (handlers) {
handlers.slice().map((handler) => {
handler(args)
})
}
},
/**
* 取消监听
*
* @param {string} key
* @param {function} callback
*/
off(key, callback) {
const handlers = eventList.get(key)
if (handlers) {
const index = handlers.indexOf(callback)
if (index !== -1) {
return new Error('not find callback')
}
handlers.splice(index, 1)
}
}
}
}