Интересная задача с собеседования

от автора

Однажды на собеседовании мне предложили решить одну интересную задачу, которая для меня была довольно необычной на тот момент.
Позже я обнаружил, что задача была не особо уникальной, но с высоты моего опыта тогда, она показалась довольно будоражащей.

Условие задачи

Создайте класс EventEmitter, который позволяет:

  • подписываться на события (on) с любым количеством функций на одно событие;

  • отписываться от конкретной функции (off), даже если функция анонимная;

  • вызывать все функции для события (emit) с передачей аргументов.

Код задачи:

class EventEmitter {   events = {};    on(name, fn) {     // здесь будет логика подписки   }    off(name, fn) {     // здесь будет логика отписки   }    emit(name, ...args) {     // здесь будет логика вызова всех функций события   } }  const ee = new EventEmitter();  // Example of using: ee.on("login", () => {   console.log("login 1"); });  ee.on("login", () => {   console.log("login 2"); });  ee.on("login", () => {   console.log("login 3"); });  ee.emit("login1"); ee.emit("login2"); ee.emit("dude", "Bob");

Мой вариант решения.

Начнём с функции on. Она должна давать возможность создавать подписку со специальным именем. Для начала (если его ещё нет) мы должны добавить поле с именем, которое получаем из аргумента name, и поместить туда массив функций.

on(name, fn) {   if (!this.events[name]) {     this.events[name] = [];   }    this.events[name].push(fn); }

Выглядит неплохо, но мы помним, что функция может быть анонимная, а нам нужно уметь отписываться именно от неё.

Так как функция — это объект (ссылочный тип), нам нужно хранить ссылку на каждую конкретную функцию. Решить это можно, если метод on будет возвращать функцию отписки:

on(name, fn) {   if (!this.events[name]) {     this.events[name] = [];   }    this.events[name].push(fn);    return () => {     this.off(name, fn);   }; }

Хорошо, но всё-таки можно улучшить. Мы должны учитывать, что:

  1. может быть несколько одинаковых функций для одного события;

  2. нам нужно уметь отписываться от конкретного экземпляра подписки.

С текущим решением может случиться следующее:

const emitter = new EventEmitter();  function handler() {   console.log("hi"); }  const off1 = emitter.on("event", handler); const off2 = emitter.on("event", handler);  // вызов off2 снимет первую подписку, а не вторую

Чтобы это исправить — вместо хранения «чистых» функций будем хранить объекты с полем fn.

on(name, fn) {   if (!this.events[name]) {     this.events[name] = [];   }    const listener = { fn };   this.events[name].push(listener);    return () => {     this.off(name, listener);   }; }

Также, такой подход позволит в будущем легко расширять функциональность (например, добавить once, priority и т.д.).


Метод off

Для отписки нам нужно убрать элемент из массива. Так как теперь мы можем передать в off как функцию, так и объект listener, нужно учесть оба варианта:

off(name, listenerOrFn) {   if (!this.events[name]) return;    const predicate = (listener) =>     listener === listenerOrFn || listener.fn === listenerOrFn;    this.events[name] = this.events[name].filter((l) => !predicate(l)); }

Метод emit

Метод emit вызывает все подписчики события с переданными аргументами.

emit(name, ...args) {   if (!this.events[name]) return;    this.events[name].forEach((listener) => listener.fn(...args)); }

Но здесь есть подводный камень: если в процессе выполнения один из обработчиков удалит себя (off), мы изменим массив прямо во время обхода. Это может привести к ошибкам. Поэтому нужно обходить копию массива (например, через spread оператор, или, как мне больше нравится — с помощью метода массива slice()):

emit(name, ...args) {   const listeners = this.events[name];   if (!listeners) return;    listeners.slice().forEach((listener) => {     listener.fn(...args);   }); }

Финальное решение

class EventEmitter {   events = {};    // подписка на событие   on(name, fn) {     if (!this.events[name]) {       this.events[name] = [];     }      const listener = { fn };     this.events[name].push(listener);      // возвращаем функцию отписки     return () => {       this.off(name, listener);     };   }    // отписка от события   off(name, listenerOrFn) {     if (!this.events[name]) return;      const predicate = (listener) =>       listener === listenerOrFn || listener.fn === listenerOrFn;      this.events[name] = this.events[name].filter((l) => !predicate(l));   }    // вызов всех функций события   emit(name, ...args) {     const listeners = this.events[name];     if (!listeners) return;      listeners.slice().forEach((listener) => {       listener.fn(...args);     });   } }

Спасибо что прочитали, буду рад комментариям и советам по возможному улучшению!


ссылка на оригинал статьи https://habr.com/ru/articles/939264/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *