EventEmitter 很適合在不修改組件狀態結構的狀況下進行組件通訊,然而它的生命週期不受 react 管理,須要手動添加/清理監聽事件很麻煩。並且,若是一個 EventEmitter 沒有使用就被初始化也會有點麻煩。html
目的
因此使用 react hooks 結合 event emitter 的目的即是node
- 添加高階組件,經過 react context 爲全部子組件注入 em 對象
- 添加自定義 hooks,從 react context 獲取 emitter 對象,並暴露出合適的函數。
- 自動清理 emitter 對象和 emitter listener。
實現
實現基本的 EventEmitter
首先,實現一個基本的 EventEmitter,這裏以前吾輩曾經就有 實現過 ,因此直接拿過來了。react
type EventType = string | number export type BaseEvents = Record<EventType, 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) return this } /** * 移除一個事件監聽程序 * @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), ) return this } /** * 移除一類事件監聽程序 * @param type 監聽類型 * @returns {@code this} */ removeByType<E extends keyof Events>(type: E) { this.events.delete(type) return this } /** * 觸發一類事件監聽程序 * @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) }) return this } /** * 獲取一類事件監聽程序 * @param type 監聽類型 * @returns 一個只讀的數組,若是找不到,則返回空數組 {@code []} */ listeners<E extends keyof Events>(type: E) { return Object.freeze(this.events.get(type) || []) } }
結合 context 實現一個包裹組件
包裹組件的目的是爲了能直接提供一個包裹組件,以及提供 provider 的默認值,不須要使用者直接接觸 emitter 對象。git
import * as React from 'react' import { createContext } from 'react' import { EventEmitter } from './util/EventEmitter' type PropsType = {} export const EventEmitterRCContext = createContext<EventEmitter<any>>( null as any, ) const EventEmitterRC: React.FC<PropsType> = (props) => { return ( <EventEmitterRCContext.Provider value={new EventEmitter()}> {props.children} </EventEmitterRCContext.Provider> ) } export default EventEmitterRC
使用 hooks 暴露 emitter api
咱們主要須要暴露的 API 只有兩個github
useListener emit
import { DependencyList, useCallback, useContext, useEffect } from 'react' import { EventEmitterRCContext } from '../EventEmitterRC' import { BaseEvents } from '../util/EventEmitter' function useEmit<Events extends BaseEvents>() { const em = useContext(EventEmitterRCContext) 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() return { useListener: <E extends keyof Events>( 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
了。api
下面是一個簡單的 Todo 示例,使用 emitter 實現了 todo 表單 與 todo 列表之間的通訊。數組
目錄結構以下dom
todo
component
TodoForm.tsx TodoList.tsx
modal
TodoEntity.ts TodoEvents.ts
Todo.tsx
Todo 父組件,使用 EventEmitterRC
包裹子組件ide
const Todo: React.FC<PropsType> = () => { return ( <EventEmitterRC> <TodoForm /> <TodoList /> </EventEmitterRC> ) }
在表單組件中使用 useEventEmitter
hooks 得到 emit
方法,而後在添加 todo 時觸發它。函數
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 的事件。
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 類型
export interface TodoEntity { title: string }
import { BaseEvents } from '../../../components/emitter' import { TodoEntity } from './TodoEntity' export interface TodoEvents extends BaseEvents { addTodo: [TodoEntity] }