Mitt:Vue3中使用事件总线

前言

Vue3 Composition API$on$off$once实例方法已经被移除,组件实例不再实现事件触发接口,导致EventBus无法使用。

很多人议论取消是对的,太多的emiton在大型项目中兼职都是噩梦,不易管理,调试起来还很麻烦等等问题。

个人觉得仁者见仁,存在即有它的意义,主要看项目需求、技术需求需不需要使用。在我们前端代码规范里明确规定,禁止使用事件总线,原因是因为我们项目代码量太大,使用起来不好维护,出现跨组件通信更多的是使用Vuex。如果需要使用,就做好取消订阅、事件名管理、统一封装使用等操作,便于后续的管理。

回归正题,由于Vue3中取消了,有没有什么办法可以继续使用?

其实都知道,EventBus就是一个事件总线,Vue3中没有提供,就找一个替代品提供对应功能就行。

这里主要介绍的是Mitt,它是一个轻量级的JavaScript事件总线库,大小只有200b,提供了基本的事件处理功能,包括事件的订阅、取消订阅和触发,还支持使用命名空间来进行更高级的事件处理。

对事件总线不是很了解的,后面来手动实现一个事件总线,这在面试中也经常会问到,是一道经典面试题。

Mitt

安装

使用npmcnpmyarn等管理安装。

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设计模式之布-订阅模式。通过发布-订阅模式,可以让生产者将事件发布到事件总线上,而其他组件(消费者)可以自行订阅事件总线上的事件,并在时间发生时接收和处理这些事件。

基本工作原理

  • 创建事件总线对象:首先,创建一个事件总线对象,它将用于管理事件的发布和订阅。
  • 注册事件订阅者 :组件可以通过订阅方法(如onsubscribe等)将自己注册为感兴趣的事件订阅者。订阅者提供一个回调函数,用于处理事件发生时的逻辑。
  • 发布事件 :当某个事件发生时,事件发布者使用事件总线对象的发布方法(如emitpublish等)发布事件。事件对象包含事件类型和相关数据。
  • 事件传递:事件总线接收到发布的事件后,会将事件传递给所有订阅了该事件类型的订阅者。这通常是通过调用订阅者提供的回调函数来实现。
  • 事件处理:订阅者收到事件后,执行回调函数进行事件处理。它们可以访问事件对象中的数据,并根据需要执行逻辑操作。

在面试中,经常会被问到这个问题,问了必定会考手写发布-订阅模式

代码

代码理解起来不难,主要就是几个功能函数。

  • 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)
      }
    }
  }
}
相关推荐
cs_dn_Jie18 分钟前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic1 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿1 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具2 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test3 小时前
js下载excel示例demo
前端·javascript·excel
Yaml43 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事3 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶3 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json