Делегирование событий в JavaScript: понятное руководство с примерами

Делегирование событий в JavaScript: понятное руководство с примерами

Делегирование событий в JavaScript: понятное руководство с примерами

Делегирование событий в JavaScript — один из самых полезных паттернов работы с DOM. Вместо того чтобы назначать обработчик каждому элементу, мы ставим один обработчик на общий контейнер и используем всплытие событий. Это экономит память, упрощает код и отлично работает с динамически добавляемыми узлами.

Что такое делегирование событий

События в браузере всплывают от конкретного узла (event.target) вверх по дереву до документа. Делегирование использует это поведение: обработчик вешается на предка и решает, для кого из потомков событие предназначалось, фильтруя по селектору или атрибуту.

Базовый пример: меню действий в списке

<ul id="users">
  <li>Алиса <button data-action="edit">Редактировать</button> <button data-action="delete">Удалить</button></li>
  <li>Боб <button data-action="edit">Редактировать</button> <button data-action="delete">Удалить</button></li>
</ul>
const list = document.getElementById('users');

list.addEventListener('click', (e) => {
  // Ищем ближайшую кнопку в цепочке всплытия
  const btn = e.target.closest('button');
  if (!btn || !list.contains(btn)) return; // клик не по кнопке внутри #users

  const action = btn.dataset.action; // edit | delete
  const item = btn.closest('li');

  if (action === 'edit') {
    console.log('Редактируем:', item.textContent.trim());
  }
  if (action === 'delete') {
    item.remove();
  }
});

// Новые элементы автоматически поддерживаются делегированием
list.insertAdjacentHTML('beforeend', `<li>Клара <button data-action="edit">Редактировать</button> <button data-action="delete">Удалить</button></li>`);

Мы повесили только один обработчик на #users. Любые новые кнопки внутри списка начнут работать без дополнительного кода — именно поэтому делегирование идеально подходит для динамических интерфейсов.

Как это работает: target и currentTarget

list.addEventListener('click', (e) => {
  console.log('target:', e.target); // исходный узел (например, <span> внутри кнопки)
  console.log('currentTarget:', e.currentTarget); // сам <ul id="users">
});

event.target — где произошел клик. event.currentTarget — элемент, на котором висит обработчик. Для безопасной фильтрации лучше использовать closest и matches.

Фильтрация по селектору: безопаснее и читабельнее

list.addEventListener('click', (e) => {
  const actionBtn = e.target.closest('[data-action]');
  if (!actionBtn || !list.contains(actionBtn)) return;

  if (actionBtn.matches('[data-action="edit"]')) {
    // ...
  } else if (actionBtn.matches('[data-action="delete"]')) {
    // ...
  }
});

closest поднимается вверх по DOM, поэтому срабатывает даже если кликнули по вложенному узлу внутри кнопки.

Делегирование не только кликов: фокус, ввод, контекстное меню

Не все события всплывают одинаково: например, focus и blur не всплывают, но есть аналоги — focusin и focusout. Либо можно использовать режим захвата.

const form = document.querySelector('#profile');

// Вариант 1: всплывающие аналоги
form.addEventListener('focusin', (e) => {
  const input = e.target.closest('input, textarea');
  if (input) input.classList.add('focused');
});
form.addEventListener('focusout', (e) => {
  const input = e.target.closest('input, textarea');
  if (input) input.classList.remove('focused');
});

// Вариант 2: режим захвата (capture)
form.addEventListener('focus', (e) => {
  // ...
}, { capture: true });

Аналогично можно делегировать change, contextmenu, keydown, input и другие события, учитывая их особенности.

Производительность: почему делегирование быстрее

  • Меньше обработчиков — меньше памяти и затрат на регистрацию.
  • Не нужно перевешивать обработчики при перерисовке списка.
  • Меньше закрытий и ссылок, которые мог бы удерживать сборщик мусора.
  • Для списков на сотни и тысячи элементов делегирование почти всегда выигрывает. Исключения — когда каждый элемент требует уникальную тяжелую логику, привязанную к замыканиям — но такое встречается редко.

    Частые ошибки и как их избежать

  • Остановка всплытия в дочерних узлах. Если где-то внутри вызывается e.stopPropagation(), делегирование выше не сработает. Используйте stopPropagation только когда понимаете последствия.
  • Неправильная проверка цели. e.target может быть вложенным элементом (например, <svg> или <span>). Всегда применяйте closest и проверяйте принадлежность контейнеру.
  • Смешивание inline-обработчиков и делегирования. Старайтесь придерживаться одного стиля — это упростит отладку.
  • Злоупотребление innerHTML. Полная замена содержимого контейнера уничтожит состояние (например, выделение, фокус). По возможности меняйте только нужные узлы.
  • Мини-практика: делегированный ToDo-лист

    <form id="todo-form">
      <input name="title" placeholder="Новая задача" required>
      <button>Добавить</button>
    </form>
    <ul id="todo"></ul>
    const formEl = document.getElementById('todo-form');
    const listEl = document.getElementById('todo');
    
    formEl.addEventListener('submit', (e) => {
      e.preventDefault();
      const title = new FormData(formEl).get('title');
      listEl.insertAdjacentHTML('beforeend',
        `<li>
          <label>
            <input type="checkbox" data-action="toggle">
            <span class="text">${title}</span>
          </label>
          <button data-action="remove">×</button>
        </li>`
      );
      formEl.reset();
    });
    
    listEl.addEventListener('click', (e) => {
      const removeBtn = e.target.closest('[data-action="remove"]');
      if (removeBtn) {
        removeBtn.closest('li').remove();
        return;
      }
    });
    
    listEl.addEventListener('change', (e) => {
      const checkbox = e.target.closest('[data-action="toggle"]');
      if (!checkbox) return;
      checkbox.closest('li').classList.toggle('done', checkbox.checked);
    });
    

    У нас один обработчик на клик и один на изменение — список может расти бесконечно, но код не усложняется.

    Что дальше изучить

  • addEventListener: опции once, passive, capture
  • Поведение редких событий: mouseenter vs mouseover
  • Делегирование на уровне документа: модальные окна, контекстные меню, хоткеи
  • Если хотите закрепить паттерны на практике и собрать мини‑проекты с нуля до продвинутого уровня, рекомендую пройти интенсив: Пройти практический курс «JavaScript с Нуля до Гуру 2.0» — там много упражнений и разборов кода с обратной связью.

    Итоги

    Делегирование событий в JavaScript — простой способ ускорить приложение, снизить расход памяти и облегчить работу с динамическим DOM. Запомните три правила: вешайте обработчик на общий контейнер, фильтруйте цель через closest, учитывайте особенности всплытия разных событий. Этого достаточно, чтобы уверенно применять прием в реальных проектах.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий