Event Emitter

Como criar um gerenciador de eventos

Introdução

Se você trabalhou com JavaScript, sabe o quanto da interação do usuário é tratada por meio de eventos: cliques do mouse, cliques de botões, entradas do teclado, reações aos movimentos do mouse e assim por diante.

Além disso, também usamos este conceito em vários outros lugares, como WebSocket e até arquiteturas que se baseiam em eventos. Vamos criar nosso próprio gerenciador de eventos e assim integrar onde fizer sentido em nossos projetos.

Para evitar confusão com eventos do DOM, usaremos nomenclaturas utilizadas no EventEmitter do NodeJS.

Requisitos

Basicamente, nosso EventEmitter será composto por 4 métodos em sua API pública, são eles:

  1. on: espera o nome do evento e uma função callback, que será executada toda vez que houver uma emissão para o evento.

  2. once: espera o nome do evento e uma função callback, que será executada apenas uma vez para o evento.

  3. off: espera o nome do evento e a função callback que não será mais executada para o evento.

  4. emit: espera o nome do evento e o valor que será emitido para as funções callback ouvindo o evento.

Implementação

Certo, vamos começar definindo nossa interface Callback.

export interface Callback<T> {
  (value: T): void
  once?: boolean
}

Definimos que Callback é uma função e possui uma flag once que usaremos para identificar se devemos remover essa função da lista de execuções depois da primeira execução.

Na classe EventEmitter, vamos começar definindo nossas propriedades e métodos privados, eles serão utilizados nos métodos da API pública.

export class EventEmitter<T> {
  #listeners = new Map()
  
  #getListeners<K extends keyof T>(type: K): Set<Callback<T[K]>> {
    return this.#listeners.get(type) ?? new Set()
  }
}

Perceba que #listeners é um Map que registra um Set de Callback para cada evento.

off

O método off será utilizado por quase todos os demais.

export class EventEmitter<T> {
  
  off<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    const listeners = this.#getListeners(type)
    listeners.delete(callback)
    this.#listeners.set(type, listeners)
  }
  
}

Veja, nós atribuímos o Set de um determinado evento em listeners, removemos a função solicitada e então sobrescrevemos o evento com o novo Set, sem a função.

on

Geralmente este é o método mais utilizado.

export class EventEmitter<T> {
  
  on<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    const listeners = this.#getListeners(type)
    this.#listeners.set(type, listeners.add(callback))
    return {off: () => this.off(type, callback)}
  }
  
}

Atribuímos o Set de um determinado evento em listeners e sobrescrevemos o evento com o novo Set, com a função adicionada.

once

Aqui precisamos definir a flag que determina quantas vezes a função será executada.

export class EventEmitter<T> {
  
  once<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    callback.once = true
    this.on(type, callback)
  }
  
}

Repare que apenas adicionamos a flag once, e aproveitamos a implementação do método on.

emit

Vamos para nosso último e 2º principal método.

export class EventEmitter<T> {

  emit<K extends keyof T>(type: K, value: T[K]) {
    const listeners = this.#getListeners(type)
    for (const fn of listeners) {
      if (fn.once) this.off(type, fn)
      fn(value)
    }
  }

}

Atribuímos o Set de um determinado evento em listeners e percorremos para a execução de cada um passando o valor a ser emitido, também removemos a função caso possua a flag once.

Código completo

export interface Callback<T> {
  (value: T): void
  once?: boolean
}

export class EventEmitter<T> {
  #listeners = new Map()
  
  on<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    const listeners = this.#getListeners(type)
    this.#listeners.set(type, listeners.add(callback))
    return {off: () => this.off(type, callback)}
  }
  
  once<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    callback.once = true
    this.on(type, callback)
  }
  
  emit<K extends keyof T>(type: K, value: T[K]) {
    const listeners = this.#getListeners(type)
    for (const fn of listeners) {
      if (fn.once) this.off(type, fn)
      fn(value)
    }
  }
  
  off<K extends keyof T>(type: K, callback: Callback<T[K]>) {
    const listeners = this.#getListeners(type)
    listeners.delete(callback)
    this.#listeners.set(type, listeners)
  }
  
  #getListeners<K extends keyof T>(type: K): Set<Callback<T[K]>> {
    return this.#listeners.get(type) ?? new Set()
  }
}

Demo

import {EventEmitter} from './event-emitter'

interface EmitterMap {
  update: number
}

const emitter = new EventEmitter<EmitterMap>()

emitter.on('update', (value) => {
  // Usa o value pra algo útil
})

emitter.emit('update', 10)

A interface EmitterMap pode conter vários eventos, definindo seus respectivos tipos de valores emitidos.

Espero que este conhecimento seja útil pra você, abraço.

Last updated