应用 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 | numberexport 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