Создание собственного EventEmitter‑класса

Храните подписки в Map и возвращайте функцию удаления конкретной регистрации. Для ожидания асинхронных обработчиков dispatchEvent должен возвращать Promise<void>.

Короткий ответ

Что ответить на собеседовании

Храните подписки в Map и возвращайте функцию удаления конкретной регистрации. Для ожидания асинхронных обработчиков dispatchEvent должен возвращать Promise.

Подробный разбор

Ответ с пояснениями

Условие

Ваша задача – написать класс EventEmitter, который будет поддерживать регистрацию обработчиков событий и их последующий вызов. Нужно реализовать два публичных метода:

  • addEventListener(event, listener) – привязывает функцию‑обработчик к указанному событию и возвращает функцию, позволяющую отписаться от этого события.
  • dispatchEvent(event, payload?) – запускает все обработчики, связанные с событием event. При вызове обработчикам может быть передан произвольный объект payload. Обработчики могут возвращать Promise, поэтому их выполнение должно поддерживать как синхронный, так и асинхронный сценарий.

Ниже приведён набор типовых объявлений и пример использования, который ваш класс должен поддерживать.

 //  
type Payload = any;  
type RemoveEventListener = () => void;  
type EventListener = (payload?: Payload) => void | Promise<void>;  

interface IEventEmitter {  
    // Подписывает слушатель к событию event,  
    // Возвращает функцию, вызов которой отписывает слушатель от события event  
    addEventListener(event: string, listener: EventListener): RemoveEventListener;  

    // Вызывает всех слушателей, привязанных к событию event  
    // При вызове слушателя дополнительным аргументом может быть передан payload (произвольные дi-dispatchEvent(event: string, payload?: Payload): void;  

    */  

class EventEmitter {}

// Создаем инстанс EventEmitter  
const button = new EventEmitter();  

// Подписываем слушатели к событию 'click'  
const removeHandleClick1 = button.addEventListener('click', () => console.log('called on click const removeHandleClick2 = button.addEventListener('click', () => console.log('called on click const removeHandleClick3 = button.addEventListener('click', () => console.log('click'));  

// Подписываем слушатель к событию 'hover'  
const removeHandleHover = button.addEventListener('hover', (payload) => console.log('called on click const removeHandleHover');  

// Оповещаем всех слушателей о наступлении события 'click'  
button.dispatchEvent('click'); // called on click 1, called on click 2  

// Оповещаем всех слушателей о наступлении события 'hover'  
button.dispatchEvent('hover', 1); // called on hover 1  

// Отписываем все слушатели  
removeHandleClick1();  
removeHandleClick2();  
removeHandleHover();  

// Пытаемся снова оповестить всех слушателей о наступлении событий 'click' и 'hover'  
button.dispatchEvent('click'); // Оборботчики не вызвались  
button.dispatchEvent('hover'); // Оборботчики не вызвались  

Ответ

Храните подписки в Map и возвращайте функцию удаления конкретной регистрации. Для ожидания асинхронных обработчиков dispatchEvent должен возвращать Promise.

В условии повреждены комментарии и строки примера. Ниже самостоятельная реализация описанного контракта; возвращаемый Promise позволяет не терять ошибки слушателей.

class EventEmitter {
  events = new Map();
  addEventListener(event, listener) {
    if (typeof listener !== 'function') throw new TypeError('Expected listener');
    const entry = { listener };
    const listeners = this.events.get(event) ?? new Set();
    listeners.add(entry);
    this.events.set(event, listeners);
    return () => {
      listeners.delete(entry);
      if (!listeners.size && this.events.get(event) === listeners) this.events.delete(event);
    };
  }
  async dispatchEvent(event, payload) {
    const snapshot = [...(this.events.get(event) ?? [])];
    await Promise.all(snapshot.map(({ listener }) => Promise.resolve().then(() => listener(payload))));
  }
}

Обработчики запускаются из снимка параллельно, изменения подписок влияют на следующую отправку. Отписка идемпотентна; одинаковую функцию можно зарегистрировать независимо дважды. Вызов используйте с await или .catch(). Для контракта строго void пришлось бы отдельно определить канал обработки асинхронных ошибок.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее