Search
Write a publication
Pull to refresh

Перестаньте использовать CustomEvent

Reading time5 min
Views5K
Original author: Justin Fagnani

Привет, Хабр! Это моя первая статься, хоть и перевод, но все же, готов выслушать конструктивную критику)

Я часто вижу, как веб-разработчики используют CustomEvent в коде своих компонентов. Настолько часто, что у многих складывается впечатление, будто CustomEvent — единственный способ создавать custom события (с маленькой "c"), а то и вообще единственный способ генерировать собственные события.

Это понятно. Это прямо указано в названии: "Пользовательское" событие. Создается впечатление, что это идеальный инструмент для этой задачи. Это даже звучит созвучно с "пользовательским компонентом". Но я всегда говорю разработчикам, не использовать CustomEvent. Нет ни одной причины это делать. Почему?

Что не так с CustomEvent?

Во-первых. Будем честны: использование CustomEvent — это не катастрофа. Среди плохих практик веб-разработки это даже не девятиалармный пожар — не больше, чем если бы вы оставили гореть свечу. CustomEvent работает, и его использование не нанесёт вреда самым важным пользователям — конечным пользователям приложения.

Тем не менее, я довольно часто критикую это в кругах веб-разработчиков, так что, наверное, лучше собрать все свои аргументы в одном месте.

CustomEvent не должно было существовать!

CustomEvent существует только потому, что на момент его появления наследование от нативных классов не поддерживалось. Добавление объекта detail через параметры инициализации события было рекомендованным платформой способом передачи payload в событие. Я полагаю, что можно было просто добавлять свойства к любому экземпляру Event, но, возможно, из-за особенностей спецификации или браузера Internet Explorer нельзя было гарантировать, что один и тот же экземпляр события дойдёт до всех обработчиков.

Затем вышел ES2015, и наконец появилась возможность наследоваться от нативных классов — таких как Array, HTMLElement и Event. Если бы ES6 (тот, что когда-то называли ES2015) появился раньше, CustomEvent просто не понадобился бы. Об этом мне лично рассказывали инженеры браузеров, работавшие над DOM API в то время.

CustomEvent - уязвимая деталь реализации

Когда вы используете CustomEvent, вы заставляете пользователя вашего компонента — разработчика — обращаться к свойству detail. Им нужно знать, что за данные спрятаны внутри этой искусственной обёртки.

// Удобное использование CustomEvent для разработчиков
someElement.addEventListener('my-event', (e: CustomEvent<MyEventDetail>) => {
  // К чему этот дополнительный шаг?
  const { foo, bar } = e.detail;
  // ...
});

Это непрозрачная абстракция. Пользователям вашего кода не нужно знать, что вы выбрали CustomEvent — событие должно восприниматься так же естественно, как встроенные события вроде MouseEvent или KeyboardEvent.

Что использовать вместо CustomEvent

Итак, что же следует использовать вместо этого и почему?

Очевидно, что CustomEvent появился из-за отсутствия поддержки наследования в ранних версиях JavaScript, поэтому основная альтернатива — наследование от встроенного класса Event. Но об этом я расскажу чуть позже. Сначала предлагаю рассмотреть варианты без наследования: использование обычного Event или одного из стандартных встроенных типов событий.

Вам правда нужны пользовательские данные?

Конструктор Event принимает имя события и опции, такие как bubbles (всплытие) и composed (композиция). Если вашему событию не нужны дополнительные данные, вы можете просто создать экземпляр обычного Event.

На самом деле, большинству CustomEvent не нужен объект detail. Вспомните: многие стандартные события — это просто уведомления о том, что что-то произошло, и ссылка на элемент, на котором это произошло. Например, когда элемент генерирует событие 'input', оно не передаёт новое значение. Вместо этого вы обращаетесь к целевому элементу и получаете текущее значение напрямую из него.

Преимущество такого способа получения данных заключается в том, что он устойчив к изменению значения после обработки события. Вы можете взять код, который работает синхронно с 'input' событием, обернуть его в функцию debounce, и он все равно будет работать.

События без полезной нагрузки также являются более универсальными и могут использоваться повторно. Вы можете вызвать 'input' событие из элемента, который имеет другой тип данных или содержит несколько соответствующих данных, без изменения события. Многие платформенные события не содержат полезной нагрузки и представляют собой простой Event экземпляр, например 'input''change''select''load'...

Итак, для начала я бы подумал, можно ли положиться на event.target вместо полезной нагрузки.

Это субъективный выбор, потому что многим людям удобнее использовать данные, полученные непосредственно во время события. А может быть, вам действительно нужно записать данные в том виде, в котором они были получены во время события. Это веские причины для использования пользовательской полезной нагрузки.

Кроме того, вы можете создать подкласс Event даже если у вас нет пользовательской полезной нагрузки, ради некоторых других преимуществ, о которых я расскажу ниже.

Существует ли какое-то нативное событие которое вам подходит?

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

Например, когда компонент что-то загружает, вы можете запустить load событие. Или, если вы хотите сообщить об ошибке в компоненте, вы можете запустить ErrorEvent. Если у вашего компонента есть состояния «открыто» и «закрыто», рассмотрите возможность запуска ToggleEvent.

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

Подкласс Event

Поскольку мы можем создавать подклассы Event , когда нам нужно, это моя основная и наиболее общая рекомендуемая альтернатива CustomEventEvent подклассы универсальны.

Это пример моего базового шаблона для создания подклассов Event:

/**
 * Событие, которое воспроизводится при возникновении foo.
 */
export class MyEvent extends Event {
  static readonly eventName = 'my-event';

  readonly foo: number;
  readonly bar: string;

  constructor(foo: number, bar: string) {
    super(MyEvent.eventName, { bubbles: true, composed: true });
    this.foo = foo;
    this.bar = bar;
  }
}

Посмотрите, что это нам даёт.

Во-первых, он проще в использовании и выглядит приятнее. Данные о событии находятся непосредственно в объекте события. Это похоже на обычное событие.

// Пример с использованием подкласс Event.
someElement.addEventListener(MyEvent.eventName, (e: MyEvent) => {
  // Выглядит намного чище.
  const { foo, bar } = e;
  // ...
});

Во-вторых, подкласс события служит его собственной документацией. В определении класса естественным образом используется JSDoc для пояснения назначения события и значения его свойств.

В-третьих, и это самое важное для корректности программы, он обеспечивает единый источник достоверной информации о создании события. Конструктор гарантирует, что при каждом создании и отправке MyEvent будет использоваться одно и то же имя и одни и те же параметры, например bubbles и composed. Вы могли бы добиться чего-то подобного с помощью фабричной функции для CustomEvent, но в этом случае вы просто пишете обычный класс.

Наконец, подкласс Event является отдельным типом. Это огромное преимущество для пользователей TypeScript. Вместо того чтобы возиться с CustomEvent<MyDetailType>, вы просто используете сам класс события в качестве типа. Это проще и точнее.

Во многих случаях имеет смысл зарегистрировать тип события на глобальном уровне:

declare global {
  interface GlobalEventHandlersEventMap {
    'my-event': MyEvent;
  }
}

Тогда разработчики будут автоматически получать типизированные обработчики событий при использовании addEventListener(). Конечно, то же самое можно реализовать и с CustomEvent — так что в обоих случаях стоит сделать это одинаково.

Tags:
Hubs:
+3
Comments7

Articles