Однажды на собеседовании мне предложили решить одну интересную задачу, которая для меня была довольно необычной на тот момент.
Позже я обнаружил, что задача была не особо уникальной, но с высоты моего опыта тогда, она показалась довольно будоражащей.
Условие задачи
Создайте класс 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); }; }
Хорошо, но всё-таки можно улучшить. Мы должны учитывать, что:
-
может быть несколько одинаковых функций для одного события;
-
нам нужно уметь отписываться от конкретного экземпляра подписки.
С текущим решением может случиться следующее:
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/
Добавить комментарий