Перейти к содержимому

92. Делегирование событий

Делегирование событий (Event Delegation) — техника, при которой один обработчик устанавливается на родительский элемент вместо обработчиков на каждый дочерний. Работает благодаря механизму всплытия событий (bubbling).

Когда событие происходит на элементе, оно «всплывает» вверх по DOM-дереву:

span → li → ul → div → body → html → document → window
document.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)
}
})

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-action
const 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('Написать код')
// Один обработчик обслуживает все элементы, включая добавленные!
el.addEventListener('click', (e) => {
e.stopPropagation() // ✋ Останавливает всплытие (ломает делегирование!)
e.preventDefault() // 🚫 Отменяет действие браузера (ссылка, форма)
e.stopImmediatePropagation() // ✋✋ Также блокирует другие обработчики на элементе
})
// Правило: не злоупотребляйте stopPropagation — это ломает делегирование!
// Вместо этого проверяйте e.target в родительском обработчике