Поэтому я решил реализовать базовую систему событий на JavaScript.
Моей целью было поддержать следующие функции:
on
— позволяет пользователю зарегистрировать обработчик в системе событий, который срабатывает до тех пор, пока он не будет удален.
once
— то же, что и выше, но срабатывает только в первый раз, а затем удаляется.
emit
— создает событие и запускает все соответствующие обработчики.
delete
— удаляет указанный обработчик из системы.
class HandlerSet extends Map {
set(handler, once = false) {
if (typeof handler !== 'function') return
super.set(handler, !once ? handler : (...args) => {
handler(...args)
this.delete(handler)
})
}
forEach(...args) {
[...this.values()].forEach(...args)
}
}
class EventSystem extends Map {
set(type) {
return super.set(type, new HandlerSet())
}
on(type, handler, once = false) {
(this.get(type) ?? this.set(type).get(type)).set(handler, once)
}
once(type, handler) {
this.on(type, handler, true)
}
emit(type, ...args) {
this.get(type)?.forEach(handler => handler(...args))
}
delete(type, handler) {
let set = this.get(type)
if (set?.delete(handler) && set?.size === 0) {
super.delete(type)
}
}
}
Мне любопытно узнать, что думают люди об этой реализации.
Насколько мне известно, он должен быть достаточно надежным с точки зрения безопасности типов, а также вполне предсказуемым с точки зрения жизненного цикла.
Если кто-то сможет найти здесь какие-либо ошибки или предложить улучшения, я хотел бы услышать от вас!
2 ответа
Я не смог найти очевидных ошибок, так что это хорошо!
По поводу улучшений у меня есть несколько комментариев. В основном это касается удобочитаемости, и, конечно же, это просто мой субъективный опыт, когда я читаю код:
- Если обработчик не является функцией, вы должны выдать ошибку. Очевидно, это ошибка.
- При расширении базовых классов не следует переписывать сигнатуры расширенных методов. Это будет очевидно в машинописном тексте, где вы получите сообщение об ошибке. Причина в том, что было бы законно назначать EventSystem для Map, но это не сработает, поскольку EventSystem не является Map (из-за перезаписи).
- То, как реализована EventSystem, предоставляет гораздо больше методов, чем нужно. Зачем выставлять набор? Кроме того, он покажет все другие методы на карте, которые могут вам понадобиться, а могут и не понадобиться.
- В HandlerSet вам, кажется, нужна структура данных, имитирующая Set. Карта — это деталь реализации, и она работает довольно хорошо. Однако, когда вы добавляете элемент в обычный набор, этот метод называется «добавить». Это мелочь, но такие мелочи случаются, когда вы расширяете базовый класс, который не довольно соответствовать.
- В методе «on» у вас есть что-то похожее на «получить, установить, установить» с некоторыми скобками, которые необходимо мысленно упорядочить. Читать сложнее, чем нужно, по крайней мере, на мой взгляд. Я бы разделил его на две строки, чтобы сделать его совершенно очевидным.
- HandlerSet.forEach мне кажется немного странным. Вы расширили Map, так почему бы не использовать методы Map непосредственно из EventSystem? Или, если нет, почему бы не создать функцию, которая действительно вызывает обработчики? Расширение Map в HandlerSet совершенно бессмысленно, поскольку вы не используете ни одного метода из Map (кроме delete). Карта должна быть деталью реализации imo.
События или сообщения?
Это система обмена сообщениями или система событий. На мой взгляд, это система обмена сообщениями.
Предполагая, что это для браузера, вы можете предпочесть предоставленный events.Event API, который будет использовать стек событий. ПРИМЕЧАНИЕ перед использованием проверьте сопоставимость.
Рассмотрение
Обзор кода общего стиля. Стиль кода субъективен, и поэтому эти моменты являются всего лишь предложениями.
По возможности используйте
instanceof
скорее, чемtypeof
поскольку у первого значительно меньше накладных расходовНапример
if (typeof handler === 'function') {
может быть
if (handler instanceof Function) {
Не заглушайте ошибки (о, я имею в виду … «НИКОГДА !!!). Вышеупомянутая точка (проверка обратного вызова) отключит код, передающий неправильный тип объекта. Отключение звука означает, что ошибка может остаться незамеченной во время разработки.
Либо бросьте при проверке, либо позвольте JS бросить естественным образом.
Всегда разделяйте блоки кода с помощью
{}
Напримерif (foo) return
должно бытьif (foo) { return }
Используйте точки с запятой!
По возможности используйте константы. У тебя есть
let set = this.get(type)
должно бытьconst set = ...
Избегайте повторяющихся положений
У вас есть заявление
if (set?.delete(handler) && set?.size === 0) {
Второй
?.
является избыточным. Если первое предложение верно, тоset
не должно бытьundefined
.Также используйте логическое не
!
скорее, чем=== 0
.Map.size
будет только когда-либоNumber
таким образом!set.size
всегда будетtrue
для0
if (set?.delete(handler) && !set.size) {
Избегайте промежуточных вызовов, особенно при использовании параметров rest, поскольку это заставляет накладные расходы на создание массива просто передавать аргументы между функциями.
У тебя есть
class HandlerSet extends Map { forEach(...args) { [...this.values()].forEach(...args); } class EventSystem extends Map { emit(type, ...args) { this.get(type)?.forEach(handler => handler(...args)); }
Может быть…
emit(type, ...args) { const handlers = this.get(type); if (handlers) { for (const handler of handlers.values()) { handler(...args) } } }
… сокращение накладных расходов на память и итерацию. Также устраняет необходимость в
HandlerSet.forEach
Будьте осторожны с именами.
Избегайте именования переменных по их типу. Назовите переменные то, что они представляют, а не то, что они есть.
Я считаю, что ваш выбор имени запутывает функциональность вашего кода. Основная проблема в том, что понятия слушателя и события были размыты. У события есть имя, например, событие загрузки будет вызываться
"load"
. Каждое именованное событие имеет набор слушателей / обратных вызовов (функций)Примечания к некоторым именам, которые вы использовали
handler
Обычный JS использует прослушиватели, а не обработчики, однако в этом случае они больше похожи на обратные вызовы,listener
или жеcallback
было бы более подходящим (ПРИМЕЧАНИЕcallback
как одно слово неcallBack
).HandlerSet
расширяетMap
. Имена не должны включать описания типов. Добавление имени подобного типа просто сбивает с толку.EventSystem
это действительно система? Не добавляйте к именам лишнего содержания. Может бытьGlobalEvents
было бы лучше?EventSystem.on
двусмысленное имя, может бытьaddListener
,addCallback
, или даже простоaddCall
?EventSystem.once
Похожий наon
. Личноonce
функциональность неуместна или может быть обработана с помощью параметров при добавлении слушателя.EventSystem.emit
Соглашение JS заключается в том, что события запускаются, а не генерируются, поэтому, возможно,fire
было бы лучшеEventSystem.delete
Удалить что, событие или слушателя? Имена никогда не должны быть двусмысленными.
Переписать? … Нет.
Я переписал ваш код, но не смог согласовать его полезность из-за двусмысленности (предполагаемых) вариантов использования.
Таким образом, то, что следует ниже, является примером и всего лишь модификацией кода, который я использую для добавления сообщений.
Пример
Примечание код примера был проанализирован, но не протестирован.
Примечание то Map
названный events
закрывается и, таким образом, защищается от неправильного использования.
Примечание Это не проверяет, является ли слушатель функцией. Это может скрыть / заглушить плохой код. Если cb не является функцией, он будет вставлять fireEvent
// Naming: cb and cbs short for "callback" (a function) and "callbacks" (array of callback objects)
function Events(owner = {}) {
const events = new Map(); // named arrays of listener objects
return Object.assign(owner, {
addListener(name, cb, options = {}) {
var cbs;
((events.get(name) ?? (events.set(name, cbs = []), cbs)).push({cb, options}));
},
removeEvent(name) { events.delete(name) },
removeListener(name, cb) {
const cbs = events.get(name);
if (cbs?.length) {
const idx = cbs.findIdx(listener => listener.cb === cb);
idx > -1 && cbs.splice(idx, 1);
}
},
fireEvent(name, data = {}) {
var i = 0;
const cbs = events.get(name);
if (cbs?.length) {
for (const {cb, options} of cbs) {
options.once && cbs.splice(i--, 1);
cb.bind(owner)({type: name, target: owner, ...data});
i++;
}
}
},
});
}
export {Events};
Хороший довод в пользу передачи в набор не функции и того факта, что она должна выдавать ошибку. HandlerSet изначально расширял набор, однако мне нужен был способ найти функцию-обработчик в наборе, даже если он был обернут функцией очистки, как видно, когда bool «once» передается в функцию набора. Когда он завернут, его больше нельзя найти с помощью исходной функции. Что касается переопределения функций и изменения параметров, насколько мне известно, это не ошибка JavaScript, скорее я просто использую функцию языка.
— шестигранник
Я также согласен, что реализация функции «on» должна быть скорректирована, чтобы сделать ее более читаемой. И я также согласен с изменением переопределения forEach, чтобы вместо этого вызывать функции напрямую, спасибо за всесторонний обзор!
— шестигранник
Использование карты для набора обработчиков совершенно нормально, это просто расширение, которое мне не понравилось. Вы можете назвать это охватом языковой функции, если хотите, но только потому, что javascript позволяет вам делать всевозможные сумасшедшие вещи, не означает, что это хорошая идея. Я обозначил некоторые проблемы, но вам решать, важны они или нет. См. Также принцип «подстановки лисков», если хотите узнать больше.
— Магнус Джеффс Товслид