11

使用 React Context 结合 EventEmitter

 3 years ago
source link: https://blog.rxliuli.com/p/4358e813/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 React Context 结合 EventEmitter

EventEmitter 很适合在不修改组件状态结构的情况下进行组件通信,然而它的生命周期不受 react 管理,需要手动添加/清理监听事件很麻烦。而且,如果一个 EventEmitter 没有使用就被初始化也会有点麻烦。

所以使用 react context 结合 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, PropsWithChildren } from 'react'
import { BaseEvents, EventEmitter } from './util/EventEmitter'

export const EventEmitterContext = createContext<EventEmitter<any>>(null as any)

export function EventEmitterRC<T extends BaseEvents>(
  props: PropsWithChildren<{ value: EventEmitter<T> }>,
) {
  return (
    <EventEmitterContext.Provider value={props.value}>
      {props.children}
    </EventEmitterContext.Provider>
  )
}

使用 hooks 暴露 emitter api

我们主要需要暴露的 API 只有三个

  • useListener: 添加监听器,使用 hooks 是为了能在组件卸载时自动清理监听函数
  • emit: 触发监听器,直接调用即可
  • emitter: 在当前组件树生效的 emitter 对象
import {
  DependencyList,
  useCallback,
  useContext,
  useEffect,
  useMemo,
} from 'react'
import { EventEmitterContext } from '../EventEmitterRC'
// noinspection ES6PreferShortImport
import { BaseEvents, EventEmitter } from '../util/EventEmitter'

function useEmit<Events extends BaseEvents>() {
  const em = useContext(EventEmitterContext)
  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<Events>()
  // 这里使用 useMemo 产生的 emitter 对象的原因是在当前组件树 emitter 仅初始化一次
  const emitter = useMemo(() => new EventEmitter<Events>(), [])
  return {
    useListener: <E extends keyof Events>(
      type: E,
      listener: (...args: Events[E]) => void,
      deps: DependencyList = [],
    ) => {
      const em = useContext(EventEmitterContext)
      useEffect(() => {
        console.log('emitter add: ', type, listener.name)
        em.add(type, listener)
        return () => {
          console.log('emitter remove: ', type, listener.name)
          em.remove(type, listener)
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [listener, type, ...deps])
    },
    emit,
    emitter,
  }
}

使用起来非常简单,在需要使用的 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> = () => {
  const { emitter } = useEventEmitter()
  return (
    <EventEmitterRC value={emitter}>
      <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: [entity: TodoEntity]
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK