Back

Руководство разработчика по пользовательским событиям JavaScript

Руководство разработчика по пользовательским событиям JavaScript

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

Это руководство охватывает создание и отправку пользовательских событий, передачу структурированных данных через payload detail объекта CustomEvent и использование EventTarget в качестве лёгкой шины событий. Вы также узнаете, как пользовательские события работают в Web Components и как они распространяются через границы Shadow DOM.

Ключевые моменты

  • Используйте конструктор CustomEvent со свойством detail для передачи структурированных данных через события
  • Интерфейс EventTarget работает автономно, обеспечивая лёгкие паттерны pub/sub без DOM-элементов
  • Устанавливайте bubbles: true для событий, которые должны всплывать вверх по дереву DOM
  • Используйте composed: true, чтобы позволить событиям пересекать границы Shadow DOM в Web Components

Создание и отправка пользовательских событий

Конструктор CustomEvent — это современный способ создания пользовательских событий. Забудьте про initCustomEvent — это устаревший API, который вам не понадобится в современных браузерах.

const event = new CustomEvent('user-login', {
  detail: { userId: 123, timestamp: Date.now() },
  bubbles: true,
  cancelable: true
})

document.querySelector('#app').dispatchEvent(event)

Здесь важны три опции:

  • detail: ваша структурированная полезная нагрузка (любые сериализуемые данные)
  • bubbles: всплывает ли событие вверх по дереву DOM
  • cancelable: могут ли слушатели вызвать preventDefault()

Прослушивание работает точно так же, как с нативными событиями:

document.querySelector('#app').addEventListener('user-login', (e) => {
  console.log(e.detail.userId) // 123
})

Payload detail в CustomEvent

Свойство detail — это то, где пользовательские события превосходят базовый конструктор Event. Хотя технически вы можете присваивать произвольные свойства объекту Event после создания, detail предоставляет выделенное пространство имён для ваших данных без конфликтов.

const cartEvent = new CustomEvent('cart-updated', {
  detail: {
    items: [{ id: 1, qty: 2 }, { id: 3, qty: 1 }],
    total: 59.99,
    currency: 'USD'
  }
})

Обработчики получают доступ к этому через event.detail — чисто и предсказуемо.

DOM EventTarget как шина событий

Вам не нужен DOM-элемент для использования событий. Интерфейс EventTarget работает автономно, что делает его идеальным для лёгкого механизма pub/sub:

class AppEventBus extends EventTarget {
  emit(eventName, data) {
    this.dispatchEvent(new CustomEvent(eventName, { detail: data }))
  }
  
  on(eventName, handler) {
    this.addEventListener(eventName, handler)
  }
  
  off(eventName, handler) {
    this.removeEventListener(eventName, handler)
  }
}

const bus = new AppEventBus()
bus.on('notification', (e) => console.log(e.detail.message))
bus.emit('notification', { message: 'Hello!' })

Этот паттерн шины событий на основе EventTarget сохраняет компоненты слабо связанными без внешних зависимостей. Для пользователей TypeScript вы можете типизировать payload detail с помощью дженериков: CustomEvent<{ message: string }>.

Примечание: В Node.js EventTarget существует, но CustomEvent доступен как глобальный объект только в Node 19+. EventEmitter в Node остаётся более распространённым паттерном.

Пользовательские события в Web Components

Web Components активно используют пользовательские события для внешней коммуникации. Компонент отправляет события, а родительский код их прослушивает:

class UserCard extends HTMLElement {
  connectedCallback() {
    this.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('user-selected', {
        detail: { id: this.dataset.userId },
        bubbles: true,
        composed: true
      }))
    })
  }
}
customElements.define('user-card', UserCard)

Родитель прослушивает без знания внутренностей компонента:

document.querySelector('user-card').addEventListener('user-selected', (e) => {
  loadUserProfile(e.detail.id)
})

Пользовательские события Shadow DOM и опция composed

Пользовательские события Shadow DOM ведут себя по-разному в зависимости от опции composed:

  • composed: false (по умолчанию): событие останавливается на границе shadow root. Внутренняя реализация остаётся скрытой.
  • composed: true: событие пересекает границы shadow и всплывает через light DOM.
// Внутри shadow DOM
this.shadowRoot.querySelector('button').dispatchEvent(
  new CustomEvent('internal-action', {
    bubbles: true,
    composed: true // Выходит за границу shadow
  })
)

Когда событие пересекает границу shadow, event.target перенацеливается на host-элемент — слушатели снаружи видят компонент, а не его внутреннюю структуру.

Используйте composed: true для событий, которые должен обрабатывать внешний код. Оставляйте composed: false для внутренней коммуникации компонента.

Небольшое замечание о event.isTrusted: это свойство указывает, было ли событие сгенерировано браузером (действие пользователя) или скриптом. Это информационное свойство, а не механизм безопасности — не полагайтесь на него для контроля доступа.

Заключение

Пользовательские события JavaScript предоставляют фреймворк-агностичный способ построения слабо связанных, событийно-ориентированных архитектур. Используйте CustomEvent со структурированным payload detail для данных, применяйте EventTarget как автономную шину событий и понимайте, как bubbles и composed управляют распространением — особенно через границы Shadow DOM. Эти паттерны масштабируются от простой коммуникации компонентов до сложных микрофронтенд-архитектур.

Часто задаваемые вопросы

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

Да, но с оговорками. React не поддерживает нативно слушатели пользовательских событий в JSX, поэтому вам нужно будет использовать рефы и вручную подключать слушатели с помощью addEventListener. Vue лучше справляется с пользовательскими событиями через свою систему событий. Пользовательские события работают лучше всего при коммуникации между компонентами фреймворка и ванильными Web Components или фреймворк-агностичным кодом.

Нет. Устанавливайте bubbles в true только когда родительские элементы должны перехватить событие. Для прямой коммуникации, где вы отправляете и слушаете на одном и том же элементе, всплытие не нужно. Избыточное всплытие может привести к тому, что непредусмотренные слушатели будут перехватывать события, поэтому будьте осознанны в отношении распространения.

Используйте removeEventListener с точно такой же ссылкой на функцию, которую вы передали в addEventListener. Анонимные функции не могут быть удалены, потому что нет ссылки для передачи. Сохраняйте вашу функцию-обработчик в переменной или используйте паттерн AbortController с опцией signal для более чистой очистки.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay