使用 React Hooks 結合 EventEmitter
吾輩的 blog 原文在: https://blog.rxliuli.com/p/43...,歡迎來玩!
場景
EventEmitter 很適合在不修改組件狀態結構的情況下進行組件通信,然而它的生命週期不受 react 管理,需要手動添加/清理監聽事件很麻煩。而且,如果一個 EventEmitter 沒有使用就被初始化也會有點麻煩。
目的
所以使用 react hooks 結合 event emitter 的目的便是
- 添加高階組件,通過 react context 為所有子組件注入 em 對象
- 添加自定義 hooks,從 react context 獲取 emitter 對象,並暴露出合適的函數。
- 自動清理 emitter 對象和 emitter listener。
實現
實現基本的 EventEmitter
首先,實現一個基本的 EventEmitter,這裏之前吾輩曾經就有 實現過,所以直接拿過來了。
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 對象。
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 只有兩個
useListener: 添加監聽器,使用 hooks 是為了能在組件卸載時自動清理監聽函數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 了。
下面是一個簡單的 Todo 示例,使用 emitter 實現了 todo 表單 與 todo 列表之間的通信。
目錄結構如下
-
todo-
componentTodoForm.tsxTodoList.tsx
-
modalTodoEntity.tsTodoEvents.ts
Todo.tsx
-
Todo 父組件,使用 EventEmitterRC 包裹子組件
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]
}
參考
- Building event emitter using react hooks
- NodeJS EventEmitter API