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

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 818
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


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

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


Создайте класс 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);
});
}
}

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