使用 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
/**
* 事件总线
* 实际上就是发布订阅模式的一种简单实现
* 类型定义受到 {@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) || [])
}
}
结合 context 实现一个包裹组件
包裹组件的目的是为了能直接提供一个包裹组件,以及提供 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
使用 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() {
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]
}