Как улучшить реализацию EventEmitter с использованием Map и Set в JavaScript
Что проверяют
Понимаете ли вы, какие проблемы остаются в наивной реализации на Map и Set: отписка во время emit, once, утечки, ошибки в обработчиках. Следом обычно просят дописать once и вернуть из on функцию отписки.
Как отвечать
Базовая схема верная: Map<string, Set<Listener>>, где Set не даёт добавить один и тот же обработчик дважды, а удаление из него работает за O(1) вместо поиска по массиву. Дальше улучшения:
- функция отписки:
onвозвращает() => this.off(event, listener). Это удобно вuseEffect, где её сразу отдают как очистку; - чистка пустых наборов: если после
offнабор опустел, удалить ключ изMap. Иначе при динамических именах событий карта растёт без конца; - безопасный
emit: перебирать копию,[...set].Setпри обходе посещает элементы, добавленные по ходу, поэтому обработчик, который подписывает новую функцию на то же событие, может зациклить вызов; once: обёртка, которая снимает себя перед вызовом. Чтобыoff(event, original)работал и для неё, храните связь обёртки с оригиналом, например вMapили свойстве обёртки;- изоляция ошибок: исключение в одном обработчике не должно останавливать остальные. Вызов оборачивают в
try/catch, а ошибку пробрасывают черезqueueMicrotaskили отдельное событиеerror; - типизация: в TypeScript карта событий вида
Emitter<{ login: User; logout: void }>делаетonиemitстрого типизированными по имени события; - альтернатива: в браузере есть встроенный
EventTarget, а отписку от него удобно делать черезAbortSignal. Иногда своя реализация вообще не нужна.
Где спотыкаются
Думают, что Set сам защищает от утечек. Он держит сильные ссылки на обработчики и на всё, что они замыкают, пока вы явно не отпишетесь.
Передают в on стрелочную функцию прямо в вызове, а потом пытаются отписаться такой же новой стрелкой. Это другая ссылка, и off ничего не удалит.
Реализуют once через флаг внутри обработчика, не удаляя его из набора. Функция перестаёт что-то делать, но продолжает висеть в памяти и вызываться на каждом событии.