Что такое событие и как на него подписаться
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Страница без событий — просто картинка. Событие — это сообщение браузера о том, что уже произошло, а addEventListener — способ заранее сказать, кого позвать, когда это случится.
Пока страница открыта, браузер вслух комментирует происходящее: кнопку нажали, в поле ввели символ, окно изменило размер, картинки догрузились. Каждое такое сообщение — событие. Оно рассказывает о том, что уже случилось: помешать ему нельзя, можно только заранее договориться, кого позвать.
Функция, которую позовут, называется обработчиком. Подписка читается как обещание: «если на этом элементе случится click — вызови вот эту функцию». Дальше браузер сам следит за элементом, а мы возвращаемся к другим делам.
В примерах учебника некому щёлкать мышью, поэтому мы нажимаем кнопку из кода: button.click() порождает событие click, и обработчики срабатывают как обычно. Но это не то же самое, что нажатие человеком: у такого события isTrusted === false, ему не предшествуют pointerdown и mousedown, и действия, которые браузер разрешает только по настоящему жесту, по нему не выполнятся.
Вся подписка — это один метод: элемент.addEventListener(тип, обработчик, опции). Тип пишется строкой и без приставки on: "click", "input", "submit", "keydown". Обработчик получает единственный аргумент — объект события, куда браузер сложил подробности случившегося.
Два свойства объекта события пригодятся сразу: type — что случилось, target — на каком элементе. А ещё у части событий есть поведение по умолчанию (форма отправляется, ссылка уводит на адрес), и отменяет его вызов event.preventDefault(). Когда отмена уместна, а когда ломает привычки пользователя, разбирает статья «preventDefault: отмена действия по умолчанию».
Главная практическая ценность addEventListener: подписок на один элемент может быть сколько угодно, и они не знают друг о друге. Браузер хранит их списком и вызывает по очереди — в порядке добавления. Валидация формы, аналитика и подсветка кнопки живут в трёх независимых функциях, а не в одной перегруженной.
Есть тонкость: одинаковая пара «тот же тип + та же самая функция» добавляется только один раз. Повторный вызов с той же ссылкой ничего не меняет и ошибки не вызывает.
Дубликат ищется по ссылке на функцию, а не по её тексту. Две одинаковые с виду стрелочные функции — это две разные функции, и подпишутся обе.
До addEventListener обработчик клали в свойство: button.onclick = функция. Существует и совсем древний вариант — атрибут onclick прямо в разметке, но он перемешивает вёрстку с логикой, и его давно не пишут. Способ со свойством рабочий и короткий, только у него есть потолок: свойство одно.
Отсюда правило: чужая библиотека или ваш же модуль, написанный месяц назад, могут молча стереть обработчик — и наоборот. По умолчанию берите addEventListener, а onclick оставьте для быстрых проб в консоли.
| Что сравниваем | onclick | addEventListener |
|---|---|---|
| Сколько обработчиков | Один: новое присваивание стирает старое | Сколько угодно, по очереди |
| Как снять | button.onclick = null | removeEventListener с той же функцией |
| Опции once, capture, signal | Нет | Третий аргумент |
| Свои события через dispatchEvent | Только если у события есть свойство on… | Любой тип, включая придуманный вами |
Конфликта не будет: свойство и список подписок — разные места хранения, сработает и то и другое. Обработчик из свойства занимает в очереди позицию, на которой его назначили, поэтому здесь он идёт первым.
Подписка живёт, пока жив элемент, — или пока её не снимут. Снимает removeEventListener, и ему нужны тот же тип и та же ссылка на функцию. Анонимную стрелку, написанную прямо в вызове, снять нечем: второй такой же на вид литерал — уже другая функция. Значит, обработчик придётся назвать. Дальше этого правила у отписки своя глубина — фазы, утечки, снятие пачкой; ей отведена статья «Отписка и однократные обработчики», а здесь хватит самого правила.
Если обработчик и задумывался одноразовым, хранить ссылку не нужно — есть опция { once: true }. Браузер снимет подписку сам сразу после первого вызова.
Когда подписок много и убрать их нужно разом (например, компонент исчезает с экрана), удобно выдать всем один сигнал от AbortController и оборвать его одной строкой.
- Событие — сообщение браузера о том, что уже случилось; обработчик — функция, которую браузер позовёт, если подписаться заранее.
- Подписка — это
элемент.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. Менять последние четыре строки не нужно — они проверяют результат.
Обработчику не нужны параметры и возврат значения: он меняет переменную count, до которой дотягивается по замыканию, и обновляет текст элемента. textContent ждёт строку, поэтому число прогоняем через String().
Обратите внимание: подписка делается один раз, а срабатывает сколько угодно. Частая ошибка новичков — вешать обработчик внутри другого обработчика: тогда на каждый клик добавляется ещё одна подписка, и счётчик начинает расти как снежный ком.
Обработчик, который срабатывает один раз
Кнопка подтверждения должна отработать только на первом клике — второй и последующие уже ничего не делают. Решите двумя способами: через removeEventListener и через опцию once.
Первый способ — именованная функция, которая снимает саму себя. Имя здесь обязательно: именно по этой ссылке removeEventListener найдёт подписку в списке.
Второй способ короче: браузер снимет подписку сам, и хранить ссылку не нужно — можно писать стрелку прямо в вызове.
Вариант с флагом вроде let done = false тоже даст в консоли единицу, но обработчик при этом останется висеть на элементе и будет впустую вызываться на каждый клик. Снять подписку честнее.