应用 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
-
component
TodoForm.tsx
TodoList.tsx
-
modal
TodoEntity.ts
TodoEvents.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