Что такое событие и как на него подписаться

события≈ 6 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Страница без событий — просто картинка. Событие — это сообщение браузера о том, что уже произошло, а addEventListener — способ заранее сказать, кого позвать, когда это случится.

Разделы темы:·

Пока страница открыта, браузер вслух комментирует происходящее: кнопку нажали, в поле ввели символ, окно изменило размер, картинки догрузились. Каждое такое сообщение — событие. Оно рассказывает о том, что уже случилось: помешать ему нельзя, можно только заранее договориться, кого позвать.

Функция, которую позовут, называется обработчиком. Подписка читается как обещание: «если на этом элементе случится click — вызови вот эту функцию». Дальше браузер сам следит за элементом, а мы возвращаемся к другим делам.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В примерах учебника некому щёлкать мышью, поэтому мы нажимаем кнопку из кода: button.click() порождает событие click, и обработчики срабатывают как обычно. Но это не то же самое, что нажатие человеком: у такого события isTrusted === false, ему не предшествуют pointerdown и mousedown, и действия, которые браузер разрешает только по настоящему жесту, по нему не выполнятся.

Вся подписка — это один метод: элемент.addEventListener(тип, обработчик, опции). Тип пишется строкой и без приставки on: "click", "input", "submit", "keydown". Обработчик получает единственный аргумент — объект события, куда браузер сложил подробности случившегося.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два свойства объекта события пригодятся сразу: type — что случилось, target — на каком элементе. А ещё у части событий есть поведение по умолчанию (форма отправляется, ссылка уводит на адрес), и отменяет его вызов event.preventDefault(). Когда отмена уместна, а когда ломает привычки пользователя, разбирает статья «preventDefault: отмена действия по умолчанию».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Есть тонкость: одинаковая пара «тот же тип + та же самая функция» добавляется только один раз. Повторный вызов с той же ссылкой ничего не меняет и ошибки не вызывает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Дубликат ищется по ссылке на функцию, а не по её тексту. Две одинаковые с виду стрелочные функции — это две разные функции, и подпишутся обе.

До addEventListener обработчик клали в свойство: button.onclick = функция. Существует и совсем древний вариант — атрибут onclick прямо в разметке, но он перемешивает вёрстку с логикой, и его давно не пишут. Способ со свойством рабочий и короткий, только у него есть потолок: свойство одно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отсюда правило: чужая библиотека или ваш же модуль, написанный месяц назад, могут молча стереть обработчик — и наоборот. По умолчанию берите addEventListener, а onclick оставьте для быстрых проб в консоли.

Что сравниваемonclickaddEventListener
Сколько обработчиковОдин: новое присваивание стирает староеСколько угодно, по очереди
Как снятьbutton.onclick = nullremoveEventListener с той же функцией
Опции once, capture, signalНетТретий аргумент
Свои события через dispatchEventТолько если у события есть свойство on…Любой тип, включая придуманный вами

Конфликта не будет: свойство и список подписок — разные места хранения, сработает и то и другое. Обработчик из свойства занимает в очереди позицию, на которой его назначили, поэтому здесь он идёт первым.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Подписка живёт, пока жив элемент, — или пока её не снимут. Снимает removeEventListener, и ему нужны тот же тип и та же ссылка на функцию. Анонимную стрелку, написанную прямо в вызове, снять нечем: второй такой же на вид литерал — уже другая функция. Значит, обработчик придётся назвать. Дальше этого правила у отписки своя глубина — фазы, утечки, снятие пачкой; ей отведена статья «Отписка и однократные обработчики», а здесь хватит самого правила.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Если обработчик и задумывался одноразовым, хранить ссылку не нужно — есть опция { once: true }. Браузер снимет подписку сам сразу после первого вызова.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • Событие — сообщение браузера о том, что уже случилось; обработчик — функция, которую браузер позовёт, если подписаться заранее.
  • Подписка — это элемент.addEventListener(тип, обработчик, опции); тип пишется без приставки on, а обработчик получает объект события со свойствами type, target и методом preventDefault().
  • На один элемент вешается сколько угодно обработчиков, они вызываются в порядке подписки; пара «тот же тип + та же самая функция» добавляется однажды.
  • Свойство onclick одно на элемент: новое присваивание молча стирает старое — поэтому оно годится для проб в консоли, а не для реального кода.
  • Снять подписку можно только по той же ссылке на функцию (removeEventListener); для одноразовых есть { once: true }, для целой пачки — signal от AbortController.

1/3Функция log подписана на click дважды: button.addEventListener("click", log) написано две строки подряд. Сколько раз она выполнится при одном клике?

2/3Элементу дважды присвоили свойство onclick: сначала функцию с console.log("первый"), потом — с console.log("второй"). Что выведет клик?

3/3После button.addEventListener("click", () => console.log("жив")) вызвали button.removeEventListener("click", () => console.log("жив")) с точно таким же текстом функции. Что произойдёт при клике?

Счётчик кликов

Подпишитесь на клик по кнопке так, чтобы каждый клик увеличивал счётчик и записывал его в span. Менять последние четыре строки не нужно — они проверяют результат.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обработчику не нужны параметры и возврат значения: он меняет переменную count, до которой дотягивается по замыканию, и обновляет текст элемента. textContent ждёт строку, поэтому число прогоняем через String().

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание: подписка делается один раз, а срабатывает сколько угодно. Частая ошибка новичков — вешать обработчик внутри другого обработчика: тогда на каждый клик добавляется ещё одна подписка, и счётчик начинает расти как снежный ком.

Обработчик, который срабатывает один раз

Кнопка подтверждения должна отработать только на первом клике — второй и последующие уже ничего не делают. Решите двумя способами: через removeEventListener и через опцию once.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первый способ — именованная функция, которая снимает саму себя. Имя здесь обязательно: именно по этой ссылке removeEventListener найдёт подписку в списке.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вариант с флагом вроде let done = false тоже даст в консоли единицу, но обработчик при этом останется висеть на элементе и будет впустую вызываться на каждый клик. Снять подписку честнее.