React Front-end Инженер

React Front-end Инженер

Роадмап навыков для прокачки

DOM-события: добавление и удаление обработчиков

Browser APIОбъекты документа и страницыСобытия

Основная идея

Обработка DOM-событий — это механизм, позволяющий выполнять JavaScript-код в ответ на действия пользователя или изменения состояния страницы. Существует три способа назначить обработчик: HTML-атрибут, DOM-свойство и addEventListener.

Ключевые аспекты

  • addEventListener — современный способ, позволяет несколько обработчиков на одно событие
  • removeEventListener — удаляет обработчик, требует ссылку на ту же функцию
  • DOM-свойство (onclick) — только один обработчик на событие
  • HTML-атрибут — устаревший способ, смешивает HTML и JS
  • Объект события (event) — содержит информацию о произошедшем событии

Плюсы addEventListener

  • Множество обработчиков на одно событие
  • Возможность удаления конкретного обработчика
  • Поддержка фазы захвата (capture)
  • Опция once для однократного выполнения
  • Опция passive для оптимизации производительности

Минусы разных подходов

  • HTML-атрибуты нарушают разделение логики и представления
  • DOM-свойство позволяет только один обработчик
  • removeEventListener требует именованную функцию (анонимную не удалить)

Частые ошибки на собеседованиях

  • Пытаются удалить анонимную функцию через removeEventListener — это невозможно
  • Путают this в обработчике: в addEventListener это элемент, в стрелочной функции — внешний контекст
  • Забывают про третий аргумент addEventListener (options/useCapture)
  • Не знают про опцию once для одноразовых обработчиков

Введение и проблематика

Чтобы сделать веб-страницу интерактивной, нужно "научить" JavaScript реагировать на действия пользователя. Для этого существует механизм обработчиков событий (event handlers) — функций, которые вызываются при наступлении определённого события.

Обработчик события — это функция, которая выполняется в ответ на определённое событие (клик, ввод текста, загрузка страницы и т.д.).

Зачем нужно уметь управлять обработчиками?

  • Добавление интерактивности — реакция на клики, ввод, скролл
  • Оптимизация — удаление ненужных обработчиков для экономии памяти
  • Динамические интерфейсы — добавление/удаление обработчиков в зависимости от состояния
  • Предотвращение утечек памяти — очистка обработчиков при удалении элементов

Базовая теория

Три способа назначения обработчиков

Существует три способа назначить обработчик события:

graph LR A[Способы назначения] --> B[HTML-атрибут] A --> C[DOM-свойство] A --> D[addEventListener] B --> B1["onclick='...'"] C --> C1["element.onclick = fn"] D --> D1["element.addEventListener()"]

Способ 1: HTML-атрибут (устаревший)

html
<!-- Прямо в HTML -->
<button onclick="alert('Клик!')">Нажми меня</button>
 
<!-- Вызов функции -->
<button onclick="handleClick()">Нажми меня</button>
 
<!-- С передачей event -->
<button onclick="handleClick(event)">Нажми меня</button>
js
function handleClick(event) {
  console.log('Клик по кнопке');
  console.log(event.target); // кнопка
}
🚫

Не рекомендуется! Этот способ смешивает HTML и JavaScript, усложняет поддержку и нарушает принцип разделения ответственности.

Проблемы HTML-атрибутов

  • Код в HTML — плохая практика
  • Можно написать только простые выражения
  • Сложно отлаживать
  • Невозможно добавить несколько обработчиков

Способ 2: DOM-свойство

js
const button = document.querySelector('button');
 
// Назначение обработчика
button.onclick = function(event) {
  console.log('Клик!');
  console.log(this); // button — сам элемент
};
 
// Или через именованную функцию
function handleClick(event) {
  console.log('Клик!');
}
button.onclick = handleClick;

Удаление обработчика через DOM-свойство

js
// Удаление — просто присвоить null
button.onclick = null;

Ограничение: только один обработчик

js
const button = document.querySelector('button');
 
button.onclick = function() {
  console.log('Первый обработчик');
};
 
// Второй обработчик ПЕРЕЗАПИШЕТ первый!
button.onclick = function() {
  console.log('Второй обработчик');
};
 
// При клике выведется только "Второй обработчик"
⚠️

DOM-свойство позволяет назначить только ОДИН обработчик на каждый тип события. Повторное присваивание перезаписывает предыдущий обработчик.


Способ 3: addEventListener (рекомендуемый)

Синтаксис

js
element.addEventListener(eventType, handler, options);
ПараметрОписание
eventTypeСтрока с типом события: 'click', 'input', 'keydown'
handlerФункция-обработчик
optionsОбъект с опциями или boolean для useCapture

Базовый пример

js
const button = document.querySelector('button');
 
function handleClick(event) {
  console.log('Клик!');
  console.log(event.type);   // "click"
  console.log(event.target); // кнопка
}
 
// Добавление обработчика
button.addEventListener('click', handleClick);

Множественные обработчики

js
const button = document.querySelector('button');
 
// Можно добавить несколько обработчиков на одно событие!
button.addEventListener('click', function() {
  console.log('Первый обработчик');
});
 
button.addEventListener('click', function() {
  console.log('Второй обработчик');
});
 
button.addEventListener('click', function() {
  console.log('Третий обработчик');
});
 
// При клике выполнятся ВСЕ три в порядке добавления

Опции addEventListener

js
element.addEventListener('click', handler, {
  capture: false,  // фаза захвата (false = всплытие)
  once: true,      // обработчик сработает один раз и удалится
  passive: true,   // обработчик не будет вызывать preventDefault()
  signal: controller.signal // для отмены через AbortController
});
js
// once — обработчик автоматически удалится после первого вызова
const button = document.querySelector('button');
 
button.addEventListener('click', () => {
  console.log('Этот обработчик сработает только один раз!');
}, { once: true });
 
// Клик → "Этот обработчик сработает только один раз!"
// Повторный клик → ничего (обработчик уже удалён)

Удаление обработчиков: removeEventListener

Синтаксис

js
element.removeEventListener(eventType, handler, options);

Важное правило

🚫

Для удаления обработчика нужна ссылка на ту же самую функцию, которая была передана в addEventListener. Анонимную функцию удалить невозможно!

js
const button = document.querySelector('button');
 
// Именованная функция
function handleClick() {
  console.log('Клик!');
}
 
// Добавление
button.addEventListener('click', handleClick);
 
// Удаление — та же самая функция
button.removeEventListener('click', handleClick);

Практический пример: одноразовый обработчик (без once)

js
const button = document.querySelector('button');
 
function handleClick() {
  console.log('Клик! Обработчик удаляется...');
 
  // Удаляем сам себя
  button.removeEventListener('click', handleClick);
}
 
button.addEventListener('click', handleClick);

Удаление с учётом options

js
const button = document.querySelector('button');
 
function handleClick() {
  console.log('Capture phase click');
}
 
// Добавление в фазе захвата
button.addEventListener('click', handleClick, { capture: true });
 
// Удаление — нужно указать те же опции!
button.removeEventListener('click', handleClick, { capture: true });
 
// Или сокращённо:
button.removeEventListener('click', handleClick, true);

Контекст this в обработчиках

В обычной функции

js
const button = document.querySelector('button');
 
button.addEventListener('click', function() {
  // this = элемент, на котором висит обработчик
  console.log(this); // <button>...</button>
  this.classList.add('clicked');
});

В стрелочной функции

js
const button = document.querySelector('button');
 
button.addEventListener('click', () => {
  // this = внешний контекст (обычно window или undefined в strict mode)
  console.log(this); // Window или undefined
 
  // Используйте event.target или event.currentTarget вместо this
});
⚠️

Стрелочные функции не имеют собственного this. Если нужен доступ к элементу через this, используйте обычную функцию или event.currentTarget.

Разница между target и currentTarget

js
document.querySelector('.parent').addEventListener('click', function(e) {
  console.log(e.target);        // элемент, на котором произошёл клик
  console.log(e.currentTarget); // элемент, на котором висит обработчик
  console.log(this);            // то же, что currentTarget
});

Объект события (Event)

js
element.addEventListener('click', function(event) {
  // Тип события
  console.log(event.type); // "click"
 
  // Элементы
  console.log(event.target);        // на чём кликнули
  console.log(event.currentTarget); // где висит обработчик
 
  // Координаты (для событий мыши)
  console.log(event.clientX, event.clientY); // относительно viewport
  console.log(event.pageX, event.pageY);     // относительно документа
 
  // Модификаторы
  console.log(event.ctrlKey);  // зажат ли Ctrl
  console.log(event.shiftKey); // зажат ли Shift
  console.log(event.altKey);   // зажат ли Alt
  console.log(event.metaKey);  // зажат ли Cmd/Win
 
  // Методы
  event.preventDefault();  // отменить действие по умолчанию
  event.stopPropagation(); // остановить всплытие
});

Сравнение способов

КритерийHTML-атрибутDOM-свойствоaddEventListener
Множественные обработчикиНетНетДа
Удаление обработчикаСложноПросто (= null)removeEventListener
Фаза захватаНетНетДа
Опция onceНетНетДа
Разделение HTML/JSНетДаДа
РекомендуетсяНетДля простых случаевДа

Пограничные кейсы

Множественное добавление одного обработчика

js
const button = document.querySelector('button');
 
function handleClick() {
  console.log('Клик!');
}
 
// Добавляем один и тот же обработчик дважды
button.addEventListener('click', handleClick);
button.addEventListener('click', handleClick);
 
// При клике выведется только ОДИН раз "Клик!"
// addEventListener игнорирует дубликаты (одинаковая функция + тип + options)

Удаление во время обработки

js
const buttons = document.querySelectorAll('button');
 
buttons.forEach(button => {
  button.addEventListener('click', function handler() {
    console.log('Кнопка кликнута');
 
    // Безопасно удалять обработчик внутри него самого
    button.removeEventListener('click', handler);
  });
});

Вопросы интервьюера

Q: Как удалить анонимный обработчик события?

Напрямую никак — нет ссылки на функцию. Варианты: использовать именованную функцию, опцию { once: true }, или AbortController с опцией { signal }.

Q: Что такое опция passive и зачем она нужна?

passive: true говорит браузеру, что обработчик не будет вызывать preventDefault(). Это позволяет браузеру не ждать завершения обработчика для scroll/touch событий, делая прокрутку более плавной.

Q: Чем отличается event.target от event.currentTarget?

target — элемент, на котором фактически произошло событие. currentTarget — элемент, на котором установлен обработчик. При всплытии они могут различаться.

Q: Почему в стрелочной функции this не указывает на элемент?

Стрелочные функции не имеют собственного this, они берут его из внешнего лексического окружения. Используйте event.currentTarget или обычную функцию.


Источники