92. Делегирование событий
Делегирование событий (Event Delegation) — техника, при которой один обработчик устанавливается на родительский элемент вместо обработчиков на каждый дочерний. Работает благодаря механизму всплытия событий (bubbling).
Всплытие событий (Event Bubbling)
Заголовок раздела «Всплытие событий (Event Bubbling)»Когда событие происходит на элементе, оно «всплывает» вверх по DOM-дереву:
span → li → ul → div → body → html → document → windowdocument.querySelector('ul').addEventListener('click', (e) => { // e.target — элемент на котором произошёл клик // e.currentTarget — элемент на котором висит обработчик (ul) console.log('target:', e.target.tagName) console.log('currentTarget:', e.currentTarget.tagName)})
// Клик на li → сработает обработчик ul!// Клик на span внутри li → тоже сработает!Проблема без делегирования
Заголовок раздела «Проблема без делегирования»// ❌ Плохо: обработчик на каждый элементconst items = document.querySelectorAll('.item')items.forEach(item => { item.addEventListener('click', handleClick) // N обработчиков!})
// Проблемы:// 1. Много обработчиков = больше памяти// 2. Новые элементы нужно обрабатывать вручную// 3. Сложная очистка
// ✅ Хорошо: один обработчик на родителеdocument.querySelector('.list').addEventListener('click', handleClick)// Работает и для существующих, и для будущих элементов!Базовый паттерн делегирования
Заголовок раздела «Базовый паттерн делегирования»const list = document.querySelector('#todo-list')
list.addEventListener('click', function(event) { // Ищем нужный элемент через closest() const item = event.target.closest('.todo-item') if (!item) return // клик был не на todo-item
const deleteBtn = event.target.closest('.delete-btn') const checkbox = event.target.closest('input[type="checkbox"]')
if (deleteBtn) { item.remove() } else if (checkbox) { item.classList.toggle('completed', checkbox.checked) }})event.target.closest() — главный инструмент
Заголовок раздела «event.target.closest() — главный инструмент»closest(selector) ищет ближайшего предка (включая сам элемент), соответствующего селектору:
document.addEventListener('click', (e) => { // Обработка кнопок по data-атрибуту const button = e.target.closest('[data-action]') if (!button) return
const action = button.dataset.action const id = button.dataset.id
switch (action) { case 'edit': editItem(id); break case 'delete': deleteItem(id); break case 'toggle': toggleItem(id); break }})
// HTML:// <button data-action="delete" data-id="42">Удалить</button>// <button data-action="edit" data-id="42">Изменить</button>Паттерн: data-атрибуты как команды
Заголовок раздела «Паттерн: data-атрибуты как команды»// Мощный паттерн: обработчик команд через data-actionconst handlers = { 'load-more': (btn) => { const page = parseInt(btn.dataset.page) + 1 loadPage(page) }, 'filter': (btn) => { setFilter(btn.dataset.filter) }, 'sort': (btn) => { setSort(btn.dataset.field, btn.dataset.direction) }, 'modal-open': (btn) => { openModal(btn.dataset.modal) },}
document.addEventListener('click', (e) => { const actionEl = e.target.closest('[data-action]') if (!actionEl) return
const handler = handlers[actionEl.dataset.action] if (handler) handler(actionEl)})Делегирование с динамическим контентом
Заголовок раздела «Делегирование с динамическим контентом»// Список с CRUD — делегирование идеально для динамикиclass TodoList { #container #todos = [] #nextId = 1
constructor(container) { this.#container = container this.#setupDelegation() }
#setupDelegation() { this.#container.addEventListener('click', (e) => { const id = parseInt(e.target.closest('[data-id]')?.dataset.id) if (!id) return
if (e.target.closest('.delete-btn')) this.delete(id) if (e.target.closest('.toggle-btn')) this.toggle(id) }) }
add(text) { const todo = { id: this.#nextId++, text, done: false } this.#todos.push(todo) this.#render() return this }
delete(id) { this.#todos = this.#todos.filter(t => t.id !== id) this.#render() }
toggle(id) { const todo = this.#todos.find(t => t.id === id) if (todo) { todo.done = !todo.done; this.#render() } }
#render() { this.#container.innerHTML = this.#todos.map(t => ` <li data-id="${t.id}" style="${t.done ? 'opacity:.5;text-decoration:line-through' : ''}"> <button class="toggle-btn">✓</button> ${t.text} <button class="delete-btn">✕</button> </li> `).join('') }}
const list = new TodoList(document.querySelector('#todos'))list.add('Изучить делегирование').add('Практика').add('Написать код')// Один обработчик обслуживает все элементы, включая добавленные!stopPropagation vs preventDefault
Заголовок раздела «stopPropagation vs preventDefault»el.addEventListener('click', (e) => { e.stopPropagation() // ✋ Останавливает всплытие (ломает делегирование!) e.preventDefault() // 🚫 Отменяет действие браузера (ссылка, форма) e.stopImmediatePropagation() // ✋✋ Также блокирует другие обработчики на элементе})
// Правило: не злоупотребляйте stopPropagation — это ломает делегирование!// Вместо этого проверяйте e.target в родительском обработчике