EventEmitter 很适合在不修改组件状态结构的情况下进行组件通信,然而它的生命周期不受 react 管理,需要手动添加/清理监听事件很麻烦。而且,如果一个 EventEmitter 没有使用就被初始化也会有点麻烦。
所以使用 react hooks 结合 event emitter 的目的便是
首先,实现一个基本的 EventEmitter,这里之前吾辈曾经就有 实现过 ,所以直接拿过来了。
type EventType = string | number export type BaseEvents = Record/** * 事件总线 * 实际上就是发布订阅模式的一种简单实现 * 类型定义受到 {@link https://github.com/andywer/typed-emitter/blob/master/index.d.ts} 的启发,不过只需要声明参数就好了,而不需要返回值(应该是 {@code void}) */ export class EventEmitter { private readonly events = new Map () /** * 添加一个事件监听程序 * @param type 监听类型 * @param callback 处理回调 * @returns {@code this} */ add (type: E, callback: (...args: Events[E]) => void) { const callbacks = this.events.get(type) || [] callbacks.push(callback) this.events.set(type, callbacks) return this } /** * 移除一个事件监听程序 * @param type 监听类型 * @param callback 处理回调 * @returns {@code this} */ remove ( 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 } /** * 移除一类事件监听程序 * @param type 监听类型 * @returns {@code this} */ removeByType (type: E) { this.events.delete(type) return this } /** * 触发一类事件监听程序 * @param type 监听类型 * @param args 处理回调需要的参数 * @returns {@code this} */ emit (type: E, ...args: Events[E]) { const callbacks = this.events.get(type) || [] callbacks.forEach((fn) => { fn(...args) }) return this } /** * 获取一类事件监听程序 * @param type 监听类型 * @returns 一个只读的数组,如果找不到,则返回空数组 {@code []} */ listeners (type: E) { return Object.freeze(this.events.get(type) || []) } }
包裹组件的目的是为了能直接提供一个包裹组件,以及提供 provider 的默认值,不需要使用者直接接触 emitter 对象。
import * as React from 'react' import { createContext } from 'react' import { EventEmitter } from './util/EventEmitter' type PropsType = {} export const EventEmitterRCContext = createContext>( null as any, ) const EventEmitterRC: React.FC = (props) => { return ( {props.children} ) } export default EventEmitterRC
我们主要需要暴露的 API 只有两个
useListener emit
import { DependencyList, useCallback, useContext, useEffect } from 'react' import { EventEmitterRCContext } from '../EventEmitterRC' import { BaseEvents } from '../util/EventEmitter' function useEmit() { const em = useContext(EventEmitterRCContext) return useCallback( (type: E, ...args: Events[E]) => { console.log('emitter emit: ', type, args) em.emit(type, ...args) }, [em], ) } export function useEventEmitter () { const emit = useEmit() return { useListener: ( type: E, listener: (...args: Events[E]) => void, deps: DependencyList = [], ) => { const em = useContext(EventEmitterRCContext) useEffect(() => { console.log('emitter add: ', type, listener) em.add(type, listener) return () => { console.log('emitter remove: ', type, listener) em.remove(type, listener) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [listener, type, ...deps]) }, emit, } }
使用起来非常简单,在需要使用的 emitter hooks 的组件外部包裹一个 EventEmitterRC
组件,然后就可以使用 useEventEmitter
了。
下面是一个简单的 Todo 示例,使用 emitter 实现了 todo 表单 与 todo 列表之间的通信。
目录结构如下
todo
component
TodoForm.tsx TodoList.tsx
modal
TodoEntity.ts TodoEvents.ts
Todo.tsx
Todo 父组件,使用 EventEmitterRC
包裹子组件
const Todo: React.FC= () => { return ( ) }
在表单组件中使用 useEventEmitter
hooks 获得 emit
方法,然后在添加 todo 时触发它。
const TodoForm: React.FC= () => { const { emit } = useEventEmitter () const [title, setTitle] = useState('') function handleAddTodo(e: FormEvent ) { e.preventDefault() emit('addTodo', { title, }) setTitle('') } return ( ) }
在列表组件中使用 useEventEmitter
hooks 获得 useListener
hooks,然后监听添加 todo 的事件。
const TodoList: React.FC= () => { const [list, setList] = useState ([]) const { useListener } = useEventEmitter () useListener( 'addTodo', (todo) => { setList([...list, todo]) }, [list], ) const em = { useListener } useEffect(() => { console.log('em: ', em) }, [em]) return ( {list.map((todo, i) => (
) }- {todo.title}
))}
下面是一些 TypeScript 类型
export interface TodoEntity { title: string }
import { BaseEvents } from '../../../components/emitter' import { TodoEntity } from './TodoEntity' export interface TodoEvents extends BaseEvents { addTodo: [TodoEntity] }