本文最后更新于:2021年1月19日 下午
场景
EventEmitter 很适合在不修改组件状态结构的情况下进行组件通信,然而它的生命周期不受 react 管理,需要手动添加/清理监听事件很麻烦。而且,如果一个 EventEmitter 没有使用就被初始化也会有点麻烦。
目的
所以使用 react context 结合 event emitter 的目的便是
- 添加高阶组件,通过 react context 为所有子组件注入 em 对象
- 添加自定义 hooks,从 react context 获取 emitter 对象,并暴露出合适的函数。
- 自动清理 emitter 对象和 emitter listener。
实现
实现基本的 EventEmitter
首先,实现一个基本的 EventEmitter,这里之前吾辈曾经就有 实现过,所以直接拿过来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| type EventType = string | number
export type BaseEvents = Record<EventType, any[]>
export class EventEmitter<Events extends BaseEvents> { private readonly events = new Map<keyof Events, Function[]>()
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) return 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) ) return this }
removeByType<E extends keyof Events>(type: E) { this.events.delete(type) return this }
emit<E extends keyof Events>(type: E, ...args: Events[E]) { const callbacks = this.events.get(type) || [] callbacks.forEach((fn) => { fn(...args) }) return this }
listeners<E extends keyof Events>(type: E) { return Object.freeze(this.events.get(type) || []) } }
|
结合 context 实现一个包裹组件
包裹组件的目的是为了能直接提供一个包裹组件,以及提供 provider 的默认值,不需要使用者直接接触 emitter 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import * as React from "react" import { createContext, PropsWithChildren } from "react" import { BaseEvents, EventEmitter } from "./util/EventEmitter"
export const EventEmitterContext = createContext<EventEmitter<any>>(null as any)
export function EventEmitterRC<T extends BaseEvents>( props: PropsWithChildren<{ value: EventEmitter<T> }> ) { return ( <EventEmitterContext.Provider value={props.value}> {props.children} </EventEmitterContext.Provider> ) }
|
使用 hooks 暴露 emitter api
我们主要需要暴露的 API 只有三个
useListener
: 添加监听器,使用 hooks 是为了能在组件卸载时自动清理监听函数
emit
: 触发监听器,直接调用即可
emitter
: 在当前组件树生效的 emitter 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import { DependencyList, useCallback, useContext, useEffect, useMemo, } from "react" import { EventEmitterContext } from "../EventEmitterRC"
import { BaseEvents, EventEmitter } from "../util/EventEmitter"
function useEmit<Events extends BaseEvents>() { const em = useContext(EventEmitterContext) return useCallback( <E extends keyof Events>(type: E, ...args: Events[E]) => { console.log("emitter emit: ", type, args) em.emit(type, ...args) }, [em] ) }
export function useEventEmitter<Events extends BaseEvents>() { const emit = useEmit<Events>() 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(() => { console.log("emitter add: ", type, listener.name) em.add(type, listener) return () => { console.log("emitter remove: ", type, listener.name) 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
包裹子组件
1 2 3 4 5 6 7 8 9
| const Todo: React.FC<PropsType> = () => { const { emitter } = useEventEmitter() return ( <EventEmitterRC value={emitter}> <TodoForm /> <TodoList /> </EventEmitterRC> ) }
|
在表单组件中使用 useEventEmitter
hooks 获得 emit
方法,然后在添加 todo 时触发它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const TodoForm: React.FC<PropsType> = () => { const { emit } = useEventEmitter<TodoEvents>()
const [title, setTitle] = useState("")
function handleAddTodo(e: FormEvent<HTMLFormElement>) { e.preventDefault() emit("addTodo", { title, }) setTitle("") }
return ( <form onSubmit={handleAddTodo}> <div> <label htmlFor={"title"}>标题:</label> <input value={title} onChange={(e) => setTitle(e.target.value)} id={"title"} /> <button type={"submit"}>添加</button> </div> </form> ) }
|
在列表组件中使用 useEventEmitter
hooks 获得 useListener
hooks,然后监听添加 todo 的事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const TodoList: React.FC<PropsType> = () => { const [list, setList] = useState<TodoEntity[]>([]) const { useListener } = useEventEmitter<TodoEvents>() useListener( "addTodo", (todo) => { setList([...list, todo]) }, [list] ) const em = { useListener } useEffect(() => { console.log("em: ", em) }, [em]) return ( <ul> {list.map((todo, i) => ( <li key={i}>{todo.title}</li> ))} </ul> ) }
|
下面是一些 TypeScript 类型
1 2 3
| export interface TodoEntity { title: string }
|
1 2 3 4 5 6
| import { BaseEvents } from "../../../components/emitter" import { TodoEntity } from "./TodoEntity"
export interface TodoEvents extends BaseEvents { addTodo: [entity: TodoEntity] }
|
参考