基于mitt封装自动清理副作用的EventBus

2,825 阅读1分钟

我们都知道在Vue3中,已经无法使用Vue2中自带的$emit, $on功能实现发布订阅模式的跨组件通信功能,因此我们需要自己实现或是借助第三方库来实现,这里推荐两个库:mitt@vueuse/core中的useEventBus这个hooks。这两个库都有非常完善的TS类型支持,但存在细微的区别。

方案对比

@vueuse/core — useEventBus

@vueuse/core中的useEventBus这个hooks,虽然也能实现发布订阅,但是它把事件总线模块划分的很小:

import { useEventBus } from '@vueuse/core'

const bus = useEventBus<string>('message')

// listen to an event
const unsubscribe = bus.on((event) => {
  console.log(`news: ${event}`)
})

// fire an event
bus.emit('The Tokyo Olympics has begun')

// unregister the listener
unsubscribe()

执行useEventBus方法,一次只能创建特定的一类事件,例如以上代码,得到bus对象以后,只能对message这一类事件进行发布和订阅,如果想要创建出各类事件,那就需要执行多个useEventBus方法创建多个bus对象。

const messageBus = useEventBus<string>('message')
const clickBus = useEventBus<number>('click')
const connectBus = useEventBus<boolean>('connect')

mitt

对比mitt这个库,由它的方法创建出来的事件总线对象,可以监听分发多个事件:

import Mitt from "mitt";

const eventbus = Mitt<{
	"click": undefined;
	"message": string;
	"connect": boolean;
}>();

eventbus.on("click", () => {

});

eventbus.on("message", (str) => {

});

eventbus.on("connect", (flag) => {

});

eventbus.emit("click");
eventbus.emit("message", "hello");
eventbus.emit("connect", true);

组件中的副作用处理

什么是副作用?

简单来说就是我们想要完成某个事情,但在这个事情被完成后,随之也产生了除完成这件事件以外的其它方面的影响。

例如,我们想要给手机充电,但是当手机充满电的同时,手机的温度升高了。手机温度的升高也就是手机充电带来的副作用。

虽然这两种方案,单独拿出来正常使用都无所谓,没有谁好谁坏。但是日常在和其他同事的协同开发中,我们保不齐总会有不注意其中细节的时候:

例如在某个组件中,使用on方法注册监听了某个事件,但是该组件可能会经历多次的销毁和重新渲染挂载,如果我们不手动地在组件的onBeforeUnmount生命周期钩子函数中通过off方法注销的话,组件会在每次重新挂载之后都去重复注册监听事件,这样会导致同一个事件重复监听了好几次,监听事件里的逻辑也就重复触发了好几次,这显然是没有清除副作用后的意外情况。

sideEffect.gif

手动清除

针对这一情况有两种解决办法,一种是我们在开发的时候一定要注意在合适的时机手动注销监听事件,但是人都是很懒的,如果我们需要在多个组件中监听多个事件,我们也不得不在相应的组件销毁方法中手动注销,这样也显得很麻烦,而且完全有可能忘记掉手动注销这件事。

<script setup lang="ts">
import {eventbus} from "@/useMitt";
import {onBeforeUnmount} from "vue";

function onMessage() {
  // ...
}
eventbus.on("message", onMessage);

function onConnect() {
  // ...
}
eventbus.on("connect", onConnect);

onBeforeUnmount(() => {
  console.log("TheCom销毁");
  // 销毁message事件监听
  eventbus.off("message", onMessage);
  // 销毁connect事件监听
  eventbus.off("connect", onConnect);
});
</script>

自动清除

因此,第二种解决方案就是实现一个方法自动清理副作用,让我们在业务中只用关心事件的监听和触发。

这里基于mitt这个库去实现:

useMitt源码

import Mitt from "mitt";
import type {Handler} from "mitt";
import {onScopeDispose, getCurrentScope} from "vue";
// 需要引入定义好的事件类型
import type {EventTypes} from "./EventTypes";

const mitt = Mitt<EventTypes>();

function useMitt() {
	const scope = getCurrentScope();
	const EVENT_MAP = new Map<Handler<EventTypes[keyof EventTypes]>, keyof EventTypes>();

	/**
	 * 注册事件监听
	 * @param name
	 * @param handler
	 */
	function on<Key extends keyof EventTypes>(name: Key, handler: Handler<EventTypes[Key]>) {
		scope && EVENT_MAP.set(handler as Handler<EventTypes[keyof EventTypes]>, name);
		mitt.on(name, handler);
	}

	function emit<Key extends keyof EventTypes>(name: Key, params: EventTypes[Key]): void;
	function emit<Key extends keyof EventTypes>(name: undefined extends EventTypes[Key] ? Key : never): void;
	/**
	 * 触发事件监听
	 * @param name
	 * @param params
	 */
	function emit<Key extends keyof EventTypes>(name: undefined extends EventTypes[Key] ? Key : never, params?: EventTypes[Key]) {
		if (params) {
			mitt.emit(name, params as EventTypes[Key]);
		} else {
			mitt.emit(name);
		}
	}

	/**
	 * 移除事件监听
	 * @param name
	 * @param handler
	 */
	function off<Key extends keyof EventTypes>(name: Key, handler?: Handler<EventTypes[Key]>) {
		mitt.off(name, handler);
	}

	scope && onScopeDispose(() => {
		for (const [handler, name] of EVENT_MAP.entries()) {
			off(name, handler);
		}
		EVENT_MAP.clear();
	});

	return {
		$on: on,
		$emit: emit,
		$off: off
	};
}

export default useMitt;
export type EventTypes = {
	"message": string,
	"click": void,
}

使用

<script setup lang="ts">
import useMitt from "@/useMitt";

const {$on} = useMitt();

$on("message", (e) => {
  console.log(e);
});
</script>

sideeffect.gif

注意点

这里封装的mitt只能在vue组件中完成副作用的自动清除,无法做到在不依赖于组件的普通ts文件中完成自动清理。

vueuse中的useEventBus

之所以没有拿vueuse提供的useEventBus举例是因为它内部已经实现了和useMitt相同的功能😂,它的完善使用方式如下:

export const ON_MESSAGE: EventBusKey<string> = Symbol('on-message');
<script setup lang="ts">
import {useEventBus} from "@vueuse/core";
import type {EventBusKey} from "@vueuse/core";
import type {ON_MESSAGE} from "./Events";
  
const event = useEventBus(ON_MESSAGE);

event.on(e => {
	console.log(e);
});

event.emit("hello");
</script>