使用 React Hooks 結合 EventEmitter

EventEmitter 很適合在不修改組件狀態結構的狀況下進行組件通訊,然而它的生命週期不受 react 管理,須要手動添加/清理監聽事件很麻煩。並且,若是一個 EventEmitter 沒有使用就被初始化也會有點麻煩。html

目的

因此使用 react hooks 結合 event emitter 的目的即是node

  • 添加高階組件,經過 react context 爲全部子組件注入 em 對象
  • 添加自定義 hooks,從 react context 獲取 emitter 對象,並暴露出合適的函數。
  • 自動清理 emitter 對象和 emitter listener。

實現

實現基本的 EventEmitter

首先,實現一個基本的 EventEmitter,這裏以前吾輩曾經就有 實現過 ,因此直接拿過來了。react

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 對象。git

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 只有兩個github

useListener
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 了。api

下面是一個簡單的 Todo 示例,使用 emitter 實現了 todo 表單 與 todo 列表之間的通訊。數組

目錄結構以下dom

  • todo
    • component
      TodoForm.tsx
      TodoList.tsx
    • modal
      TodoEntity.ts
      TodoEvents.ts
    • Todo.tsx

Todo 父組件,使用 EventEmitterRC 包裹子組件ide

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]
}

參考

相關文章
相關標籤/搜索