Реализация системы событий Javascript (ES11)

Поэтому я решил реализовать базовую систему событий на 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 ответа
2

Я не смог найти очевидных ошибок, так что это хорошо!

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

  • Если обработчик не является функцией, вы должны выдать ошибку. Очевидно, это ошибка.
  • При расширении базовых классов не следует переписывать сигнатуры расширенных методов. Это будет очевидно в машинописном тексте, где вы получите сообщение об ошибке. Причина в том, что было бы законно назначать EventSystem для Map, но это не сработает, поскольку EventSystem не является Map (из-за перезаписи).
  • То, как реализована EventSystem, предоставляет гораздо больше методов, чем нужно. Зачем выставлять набор? Кроме того, он покажет все другие методы на карте, которые могут вам понадобиться, а могут и не понадобиться.
  • В HandlerSet вам, кажется, нужна структура данных, имитирующая Set. Карта — это деталь реализации, и она работает довольно хорошо. Однако, когда вы добавляете элемент в обычный набор, этот метод называется «добавить». Это мелочь, но такие мелочи случаются, когда вы расширяете базовый класс, который не довольно соответствовать.
  • В методе «on» у вас есть что-то похожее на «получить, установить, установить» с некоторыми скобками, которые необходимо мысленно упорядочить. Читать сложнее, чем нужно, по крайней мере, на мой взгляд. Я бы разделил его на две строки, чтобы сделать его совершенно очевидным.
  • HandlerSet.forEach мне кажется немного странным. Вы расширили Map, так почему бы не использовать методы Map непосредственно из EventSystem? Или, если нет, почему бы не создать функцию, которая действительно вызывает обработчики? Расширение Map в HandlerSet совершенно бессмысленно, поскольку вы не используете ни одного метода из Map (кроме delete). Карта должна быть деталью реализации imo.

  • Хороший довод в пользу передачи в набор не функции и того факта, что она должна выдавать ошибку. HandlerSet изначально расширял набор, однако мне нужен был способ найти функцию-обработчик в наборе, даже если он был обернут функцией очистки, как видно, когда bool «once» передается в функцию набора. Когда он завернут, его больше нельзя найти с помощью исходной функции. Что касается переопределения функций и изменения параметров, насколько мне известно, это не ошибка JavaScript, скорее я просто использую функцию языка.

    — шестигранник


  • Я также согласен, что реализация функции «on» должна быть скорректирована, чтобы сделать ее более читаемой. И я также согласен с изменением переопределения forEach, чтобы вместо этого вызывать функции напрямую, спасибо за всесторонний обзор!

    — шестигранник

  • Использование карты для набора обработчиков совершенно нормально, это просто расширение, которое мне не понравилось. Вы можете назвать это охватом языковой функции, если хотите, но только потому, что javascript позволяет вам делать всевозможные сумасшедшие вещи, не означает, что это хорошая идея. Я обозначил некоторые проблемы, но вам решать, важны они или нет. См. Также принцип «подстановки лисков», если хотите узнать больше.

    — Магнус Джеффс Товслид

События или сообщения?

Это система обмена сообщениями или система событий. На мой взгляд, это система обмена сообщениями.

Предполагая, что это для браузера, вы можете предпочесть предоставленный 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};

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

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