JavaScript 发布-订阅设计模式实现 React EventBus
原理
EventEmitter
主要是使用了发布订阅JavaScript
设计模式,它的流程如下:
- 消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者。
- 订阅者:去消息中心订阅自己感兴趣的消息。
- 发布者:满足条件时,通过消息中心发布消息。
知道了原理,我们自己来实现一个发布订阅模式,再去实现EventEmitter
就很简单了。
class PubSub {
constructor() {
// 一个对象存放所有的消息订阅
// 每个消息对应一个数组,数组结构如下
// {
// "event1": [cb1, cb2]
// }
this.events = {};
}
subscribe(event, callback) {
if (this.events[event]) {
// 如果有人订阅过了,这个键已经存在,就往里面加就好了
this.events[event].push(callback);
} else {
// 没人订阅过,就建一个数组,回调放进去
this.events[event] = [callback];
}
}
publish(event, ...args) {
// 取出所有订阅者的回调执行
const subscribedEvents = this.events[event];
if (subscribedEvents && subscribedEvents.length) {
subscribedEvents.forEach((callback) => {
callback.call(this, ...args);
});
}
}
unsubscribe(event, callback) {
// 删除某个订阅,保留其他订阅
const subscribedEvents = this.events[event];
if (subscribedEvents && subscribedEvents.length) {
this.events[event] = this.events[event].filter((cb) => cb !== callback);
}
}
}
场景
EventEmitter
很适合在不修改组件状态结构的情况下进行组件通信,然而它的生命周期不受React
管理,需要手动添加/清理监听事件很麻烦。而且,如果一个EventEmitter
没有使用就被初始化也会有点麻烦。
开发过 Vue 的同学应该都知道 Vue事件总线(EventBus),其实 EventEmitter 和它的实现方式都是大同小异。
目的
所以使用React Context
结合EventEmitter
的目的便是
- 添加高阶组件,通过
React Context
为所有子组件注入em
对象。 - 添加自定义
hooks
,从React Context
获取emitter
对象,并暴露出合适的函数。 - 自动清理
emitter
对象和emitter listener
实现
实现基本的EventEmitter
export type BaseEvents = Record<string, any[]>;
/**
* 事件总线
* 实际上就是发布订阅模式的一种简单实现
* 类型定义受到 {@link https://github.com/andywer/typed-emitter/blob/master/index.d.ts} 的启发,不过只需要声明参数就好了,而不需要返回值(应该是 {@code void})
*/
export class EventEmitter<Events extends BaseEvents> {
// 一个对象存放所有的消息订阅
private readonly events = new Map<keyof Events, Function[]>();
/**
* 添加一个事件监听程序
* @param type 监听类型
* @param callback 处理回调
* @returns {@code this}
*/
add<E extends keyof Events>(type: E, callback: (...args: Events[E]) => void) {
const callbacks = this.events.get(type) || [];
callbacks.push(callback);
this.events.set(type, callbacks);
}
/**
* 移除一个事件监听程序
* @param type 监听类型
* @param callback 处理回调
* @returns {@code this}
*/
remove<E extends keyof Events>(
type: E,
callback: (...args: Events[E]) => void
) {
const callbacks = this.events.get(type) || [];
this.events.set(
type,
callbacks.filter((fn: any) => fn !== callback)
);
}
/**
* 移除一类事件监听程序
* @param type 监听类型
* @returns {@code this}
*/
removeByType<E extends keyof Events>(type: E) {
this.events.delete(type);
}
/**
* 触发一类事件监听程序
* @param type 监听类型
* @param args 处理回调需要的参数
* @returns {@code this}
*/
emit<E extends keyof Events>(type: E, ...args: Events[E]) {
const callbacks = this.events.get(type) || [];
callbacks.forEach((fn) => fn(...args));
}
/**
* 获取一类事件监听程序
* @param type 监听类型
* @returns 一个只读的数组,如果找不到,则返回空数组 {@code []}
*/
listeners<E extends keyof Events>(type: E) {
// Object.freeze() 方法可以冻结一个对象。
// 一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
return Object.freeze(this.events.get(type) || []);
}
}
结合 Context 实现一个包裹组件
import { createContext, PropsWithChildren } from "react";
import { BaseEvents, EventEmitter } from "@/utils/EventEmitter";
export const EventEmitterContext = createContext<EventEmitter<any>>(
null as any
);
export function EventEmitterRC<T extends BaseEvents>(
props: PropsWithChildren<{ value: EventEmitter<T> }>
) {
const { value, children } = props;
return (
<EventEmitterContext.Provider value={value}>
{children}
</EventEmitterContext.Provider>
);
}
使用 hooks 暴露 emitter api
我们主要暴露的 API 只有三个
useListener
:添加监听器,使用hooks
是为了能在组件卸载时自动清理监听函数。emit
:触发监听器,直接调用即可。emitter
:在当前组件树生效的emitter
对象。
import {
useContext,
useCallback,
useMemo,
DependencyList,
useEffect,
} from "react";
import { EventEmitterContext } from "@/layout/EventEmitterRC";
import { BaseEvents, EventEmitter } from "@/utils/EventEmitter";
function useEmit<Events extends BaseEvents>() {
const em = useContext(EventEmitterContext);
return useCallback(
<E extends keyof Events>(type: E, ...args: Events[E]) =>
em.emit(type, ...args),
[em]
);
}
export function useEventEmitter<Events extends BaseEvents>() {
const emit = useEmit<Events>();
// 这里使用 useMemo 产生的 emitter 对象的原因是在当前组件树 emitter 仅初始化一次
const emitter = useMemo(() => new EventEmitter<Events>(), []);
return {
useListener: <E extends keyof Events>(
type: E,
listener: (...args: Events[E]) => void,
deps: DependencyList = []
) => {
const em = useContext(EventEmitterContext);
useEffect(() => {
em.add(type, listener);
return () => em.remove(type, listener);
}, [listener, type, ...deps]);
},
emit,
emitter,
};
}
使用
使用起来非常简单,在需要使用的emitter hooks
的组件外部包裹一个 EventEmitterRC
组件,然后就可以使用 useEventEmitter
了。
下面是一个简单的Todo
示例,使用emitter
实现了Todo
表单与Todo
列表之间的通信。
目录结构如下
todo
component
TodoForm.tsx
TodoList.tsx
modal
TodoEntity.ts
TodoEvents.ts
Todo.tsx
Todo
父组件,使用EventEmitterRC
包裹子组件
import { EventEmitterRC } from "@/layout/EventEmitterRC";
import { useEventEmitter } from "@/hooks/useEventEmitter";
const Todo = () => {
const { emitter } = useEventEmitter();
return (
<EventEmitterRC value={emitter}>
<TodoForm />
<TodoList />
</EventEmitterRC>
);
};
在表单组件中使用 useEventEmitter hook
获得 emit
方法,然后在添加todo
时触发它。
import { useState, FormEvent } from "react";
import { useEventEmitter } from "@/hooks/useEventEmitter";
import { TodoEvents } from "../modal/TodoEvents";
const TodoForm = () => {
const { emit } = useEventEmitter<TodoEvents>();
const [title, setTitle] = useState("");
function handleAddTodo(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
emit("addTodo", { title });
setTitle("");
}
return (
<form onSubmit={handleAddTodo}>
<label htmlFor={"title"}>标题:</label>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
id={"title"}
/>
<button type={"submit"}>添加</button>
</form>
);
};
export default TodoForm;
在列表组件中使用 useEventEmitter hooks
获得 useListener hooks
,然后监听添加todo
的事件。
import { useState } from "react";
import { useEventEmitter } from "@/hooks/useEventEmitter";
import { TodoEntity } from "../modal/TodoEntity";
import { TodoEvents } from "../modal/TodoEvents";
const TodoList = () => {
const [list, setList] = useState<TodoEntity[]>([]);
const { useListener } = useEventEmitter<TodoEvents>();
useListener("addTodo", (todo) => setList([...list, todo]), [list]);
return (
<ul>
{list.map((todo, i) => (
<li key={i}>{todo.title}</li>
))}
</ul>
);
};
export default TodoList;
下面是相关TypeScript
类型
export interface TodoEntity {
title: string;
}
import { BaseEvents } from "@/utils/EventEmitter";
import { TodoEntity } from "./TodoEntity";
export interface TodoEvents extends BaseEvents {
addTodo: [entity: TodoEntity];
}
参考文章: 使用 React Context 结合 EventEmitter 从发布订阅模式入手读懂Node.js的EventEmitter源码
转载自:https://juejin.cn/post/7085566957159710750