Объект события: target, type, currentTarget
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Обработчик получает не пустой вызов, а объект события — досье на то, что случилось: где щёлкнули, какое событие произошло и когда. Разберём поля, без которых не обходится ни один обработчик: target, currentTarget, type.
Браузер вызывает обработчик с одним аргументом — объектом события. Назвать его можно как угодно: event, e, ev. Это обычный параметр функции, имя ни на что не влияет, важен только порядок — он там всегда первый и всегда единственный.
Внутри — свойства, описывающие произошедшее, и все они только для чтения. У клика это ещё и координаты с номером кнопки мыши, у клавиатуры — код клавиши. Но четыре поля есть у любого события, от click до самодельного: type, target, currentTarget, timeStamp.
Последняя строка — не украшение. Мы не ждём, пока кто-то ткнёт в кнопку: go.click() порождает событие программно, и обработчик отрабатывает тут же, синхронно. Дальше все примеры устроены так же.
target — самый глубокий элемент, на котором событие возникло. currentTarget — элемент, на котором висит обработчик, выполняющийся прямо сейчас. Событие всплывает от target вверх по предкам, и на каждом слушателе currentTarget свой, а target всю дорогу один и тот же. Сам маршрут — погружение, цель, всплытие — разбирает статья «Всплытие и погружение»; здесь важно только, что слушателей по дороге бывает несколько.
Формула короткая: target — «где щёлкнули», currentTarget — «кто слушает». В обработчике-function тот же элемент лежит ещё и в this, но у стрелочной функции своего this нет, поэтому привыкайте писать event.currentTarget — это работает в обоих случаях.
Ссылка в currentTarget живёт ровно столько, сколько идёт обход дерева. Как только обработчик вернул управление, браузер обнуляет поле — и в отложенном коде вместо элемента оказывается null. Лечится одной строкой: сохранить нужный элемент в переменную сразу, пока он ещё на месте.
type — строка с именем события: click, input, keydown. Она нужна, когда одна и та же функция подписана на несколько типов сразу: разобрать поведение по event.type проще, чем держать три почти одинаковых обработчика.
timeStamp — сколько миллисекунд прошло от начала жизни страницы до события, дробное число. Само по себе значение бесполезно, зато разница двух timeStamp — готовый инструмент: так отличают двойной клик от двух отдельных и отбрасывают события, летящие слишком часто.
Список из двадцати пунктов, и на каждый нужен клик. Можно навесить двадцать обработчиков, а можно один — на общего родителя — и внутри посмотреть на target. Это делегирование, и держится оно ровно на разнице между target и currentTarget: слушаем родителя, а работаем с тем, куда попали. Здесь приём показан ради этой разницы; как на нём держатся панели, списки и формы — в статье «Делегирование событий».
Ловушка здесь одна, зато встречается у всех: target — самый глубокий узел, а не тот элемент, который вы держали в голове. Появилась внутри пункта иконка или span — и проверка event.target.tagName === "LI" перестаёт срабатывать. Поэтому от target всегда поднимаются вверх через closest().
Делегирование бессильно с событиями, которые не всплывают: focus, blur, load. У первых двух есть всплывающие двойники focusin и focusout, а в общем случае помогает третий аргумент addEventListener со стадией перехвата: { capture: true }.
Кроме четвёрки основных полей в объекте события есть несколько служебных. Знать их наизусть не нужно, но узнавать в чужом коде полезно.
| Поле | Что в нём |
|---|---|
| event.type | Имя события строкой: click, input, keydown |
| event.target | Элемент, на котором событие возникло |
| event.currentTarget | Элемент, чей обработчик выполняется сейчас |
| event.timeStamp | Миллисекунды от старта страницы до события |
| event.bubbles | Всплывает ли событие вверх по дереву |
| event.cancelable | Можно ли отменить действие по умолчанию |
| event.defaultPrevented | Вызывали ли уже preventDefault() |
| event.isTrusted | true — сделал человек, false — породил код |
Настоящие пользовательские события всплывают и обычно отменяемы, а вот у самодельного new Event("input") оба флага по умолчанию выключены. Из-за этого делегирование «не видит» рукотворные события, пока не передашь вторым аргументом { bubbles: true }. Кстати, у события из element.click() поле isTrusted равно false — браузер честно помечает всё, что пришло не от человека.
- Обработчик получает объект события первым и единственным аргументом; имя переменной любое, поля только для чтения.
- target — где событие возникло, currentTarget — чей обработчик выполняется сейчас: при всплытии target неизменен, currentTarget меняется на каждом уровне.
- currentTarget обнуляется сразу после обработчика — в setTimeout и промисах там уже null, поэтому элемент сохраняют в переменную.
- type позволяет одной функции обслуживать несколько типов событий, а timeStamp полезен разницей между двумя событиями, а не абсолютным значением.
- Делегирование — это один обработчик на родителе плюс event.target.closest(селектор); работает только для всплывающих событий.
1/3Обработчик клика висит на div#card. Внутри него button, внутри кнопки span, по которому и щёлкнули. Что окажется в event.target?
2/3Внутри обработчика вызвали setTimeout, а в его колбэке читают event.target и event.currentTarget. Что там будет?
3/3Один обработчик на ul, внутри каждого li лежит span с текстом. Почему проверка event.target.tagName === "LI" ненадёжна?
Одна панель — один обработчик
Кнопок на панели со временем станет больше, вешать обработчик на каждую не хочется. Сделайте один обработчик на контейнере: он печатает data-action нажатой кнопки, а на клик мимо кнопок молчит.
Слушаем контейнер, а от target поднимаемся к кнопке через closest(). Если кнопки над точкой клика нет — тихо выходим.
Проверка if (!button) return; обязательна: клик по самому контейнеру тоже попадает в обработчик, и там closest() вернёт null. Приятный бонус: кнопки можно добавлять и удалять в любой момент — обработчик про них ничего не знает, переподписывать его не нужно.
Кнопка, которая не узнаёт себя
Код должен печатать id кнопки, а печатает id иконки внутри неё — потому что клик попал в span. Почините обработчик, поменяв ровно одно слово.
Нужен элемент, на котором висит обработчик, а не точка клика — то есть currentTarget.
Вариант event.target.closest("button").id тоже вернёт «like», но он лишний: обработчик и так знает свой элемент. А this.id здесь не спасёт — у стрелочной функции своего this нет.