В Trello доска содержит сотни карточек. Если на каждую карточку вешать отдельный обработчик — это сотни слушателей событий в памяти. Вместо этого один обработчик на контейнер ловит клики по всем карточкам — это и есть делегирование событий, основанное на всплытии.
При добавлении новых элементов динамически нужно вешать на них обработчики. Делегирование решает обе проблемы: один обработчик на родителе работает для всех дочерних элементов, включая те, что появятся позже.
Когда происходит событие на элементе, оно сначала обрабатывается на нём, затем поднимается к родителю, и так до document:
click на span внутри li внутри ul
-> обработчики на span
-> обработчики на li
-> обработчики на ul
-> обработчики на body
-> обработчики на documentul.addEventListener('click', (event) => {
event.target // элемент, на котором произошёл клик (span внутри li)
event.currentTarget // элемент, на котором висит обработчик (ul)
})event.target — реальный источник. event.currentTarget — элемент с обработчиком.
element.addEventListener('click', (event) => {
event.stopPropagation() // событие не пойдёт дальше вверх
})Используй с осторожностью: stopPropagation может сломать другие обработчики выше по дереву.
По умолчанию обработчики срабатывают при всплытии. Третий аргумент true включает режим захвата при погружении (событие идёт сверху вниз):
element.addEventListener('click', handler, true) // capturing — при погружении
element.addEventListener('click', handler, false) // bubbling (по умолчанию)
element.addEventListener('click', handler, { capture: true, once: true })Один обработчик на родителе вместо N обработчиков на дочерних элементах:
// Плохо: 1000 карточек Trello — 1000 обработчиков
cards.forEach(card => card.addEventListener('click', handleCardClick))
// Хорошо: один обработчик на контейнер
board.addEventListener('click', (event) => {
const card = event.target.closest('.card')
if (!card) return // клик был не на карточке
handleCardClick(card)
})Преимущества делегирования:
taskList.addEventListener('click', (event) => {
const task = event.target.closest('li[data-id]')
if (!task) return
const action = event.target.dataset.action
if (action === 'delete') deleteTask(task.dataset.id)
if (action === 'complete') completeTask(task.dataset.id)
})Ошибка 1: не проверяем target — обработчик срабатывает на контейнере
// Неправильно — обрабатывает клики по пустому месту контейнера
ul.addEventListener('click', (e) => {
deleteItem(e.target.dataset.id) // e.target может быть сам ul!
})
// Правильно
ul.addEventListener('click', (e) => {
const li = e.target.closest('li')
if (!li) return // клик не на li — игнорируем
deleteItem(li.dataset.id)
})Ошибка 2: stopPropagation везде — ломает делегирование
// Если кнопка внутри карточки останавливает всплытие —
// обработчик на контейнере не получит событие!
btn.addEventListener('click', (e) => {
e.stopPropagation() // карточка больше не реагирует на клик по кнопке
doSomething()
})Ошибка 3: addEventListener внутри цикла — дублирование обработчиков
// Неправильно — при каждом рендере добавляются новые обработчики
function render(items) {
items.forEach(item => {
const el = getElement(item.id)
el.addEventListener('click', handler) // дублируется при каждом render()!
})
}
// Правильно — один обработчик на контейнер через делегирование
container.addEventListener('click', (e) => {
const item = e.target.closest('.item')
if (item) handler(item)
})Симуляция системы делегирования событий без реального DOM
// Симуляция делегирования событий через EventBus
function createEventSystem() {
const handlers = new Map()
return {
on(selector, event, handler) {
const key = event + ':' + selector
if (!handlers.has(key)) handlers.set(key, [])
handlers.get(key).push(handler)
},
dispatch(targetPath, event, data) {
if (data === undefined) data = {}
console.log('Event "' + event + '" от "' + targetPath + '"')
handlers.forEach(function(handlerList, key) {
const colonIdx = key.indexOf(':')
const evtName = key.slice(0, colonIdx)
const selector = key.slice(colonIdx + 1)
if (evtName === event && targetPath.includes(selector)) {
handlerList.forEach(function(h) {
h(Object.assign({ target: targetPath, selector }, data))
})
}
})
}
}
}
const board = createEventSystem()
// Один обработчик на "доску" ловит клики по любой "карточке"
board.on('.card', 'click', function(e) {
console.log('Карточка нажата:', e.target)
})
// Отдельный обработчик для кнопки удаления
board.on('.btn-delete', 'click', function(e) {
console.log('Удалить задачу:', e.id)
})
// Симулируем клики
board.dispatch('.board .card .card-title', 'click', { id: 'task-1' })
// Event "click" от ".board .card .card-title"
// Карточка нажата: .board .card .card-title
board.dispatch('.board .card .btn-delete', 'click', { id: 'task-2' })
// Карточка нажата: ...
// Удалить задачу: task-2
board.dispatch('.board .column-header', 'click')
// ничего — '.card' нет в путиВ Trello доска содержит сотни карточек. Если на каждую карточку вешать отдельный обработчик — это сотни слушателей событий в памяти. Вместо этого один обработчик на контейнер ловит клики по всем карточкам — это и есть делегирование событий, основанное на всплытии.
При добавлении новых элементов динамически нужно вешать на них обработчики. Делегирование решает обе проблемы: один обработчик на родителе работает для всех дочерних элементов, включая те, что появятся позже.
Когда происходит событие на элементе, оно сначала обрабатывается на нём, затем поднимается к родителю, и так до document:
click на span внутри li внутри ul
-> обработчики на span
-> обработчики на li
-> обработчики на ul
-> обработчики на body
-> обработчики на documentul.addEventListener('click', (event) => {
event.target // элемент, на котором произошёл клик (span внутри li)
event.currentTarget // элемент, на котором висит обработчик (ul)
})event.target — реальный источник. event.currentTarget — элемент с обработчиком.
element.addEventListener('click', (event) => {
event.stopPropagation() // событие не пойдёт дальше вверх
})Используй с осторожностью: stopPropagation может сломать другие обработчики выше по дереву.
По умолчанию обработчики срабатывают при всплытии. Третий аргумент true включает режим захвата при погружении (событие идёт сверху вниз):
element.addEventListener('click', handler, true) // capturing — при погружении
element.addEventListener('click', handler, false) // bubbling (по умолчанию)
element.addEventListener('click', handler, { capture: true, once: true })Один обработчик на родителе вместо N обработчиков на дочерних элементах:
// Плохо: 1000 карточек Trello — 1000 обработчиков
cards.forEach(card => card.addEventListener('click', handleCardClick))
// Хорошо: один обработчик на контейнер
board.addEventListener('click', (event) => {
const card = event.target.closest('.card')
if (!card) return // клик был не на карточке
handleCardClick(card)
})Преимущества делегирования:
taskList.addEventListener('click', (event) => {
const task = event.target.closest('li[data-id]')
if (!task) return
const action = event.target.dataset.action
if (action === 'delete') deleteTask(task.dataset.id)
if (action === 'complete') completeTask(task.dataset.id)
})Ошибка 1: не проверяем target — обработчик срабатывает на контейнере
// Неправильно — обрабатывает клики по пустому месту контейнера
ul.addEventListener('click', (e) => {
deleteItem(e.target.dataset.id) // e.target может быть сам ul!
})
// Правильно
ul.addEventListener('click', (e) => {
const li = e.target.closest('li')
if (!li) return // клик не на li — игнорируем
deleteItem(li.dataset.id)
})Ошибка 2: stopPropagation везде — ломает делегирование
// Если кнопка внутри карточки останавливает всплытие —
// обработчик на контейнере не получит событие!
btn.addEventListener('click', (e) => {
e.stopPropagation() // карточка больше не реагирует на клик по кнопке
doSomething()
})Ошибка 3: addEventListener внутри цикла — дублирование обработчиков
// Неправильно — при каждом рендере добавляются новые обработчики
function render(items) {
items.forEach(item => {
const el = getElement(item.id)
el.addEventListener('click', handler) // дублируется при каждом render()!
})
}
// Правильно — один обработчик на контейнер через делегирование
container.addEventListener('click', (e) => {
const item = e.target.closest('.item')
if (item) handler(item)
})Симуляция системы делегирования событий без реального DOM
// Симуляция делегирования событий через EventBus
function createEventSystem() {
const handlers = new Map()
return {
on(selector, event, handler) {
const key = event + ':' + selector
if (!handlers.has(key)) handlers.set(key, [])
handlers.get(key).push(handler)
},
dispatch(targetPath, event, data) {
if (data === undefined) data = {}
console.log('Event "' + event + '" от "' + targetPath + '"')
handlers.forEach(function(handlerList, key) {
const colonIdx = key.indexOf(':')
const evtName = key.slice(0, colonIdx)
const selector = key.slice(colonIdx + 1)
if (evtName === event && targetPath.includes(selector)) {
handlerList.forEach(function(h) {
h(Object.assign({ target: targetPath, selector }, data))
})
}
})
}
}
}
const board = createEventSystem()
// Один обработчик на "доску" ловит клики по любой "карточке"
board.on('.card', 'click', function(e) {
console.log('Карточка нажата:', e.target)
})
// Отдельный обработчик для кнопки удаления
board.on('.btn-delete', 'click', function(e) {
console.log('Удалить задачу:', e.id)
})
// Симулируем клики
board.dispatch('.board .card .card-title', 'click', { id: 'task-1' })
// Event "click" от ".board .card .card-title"
// Карточка нажата: .board .card .card-title
board.dispatch('.board .card .btn-delete', 'click', { id: 'task-2' })
// Карточка нажата: ...
// Удалить задачу: task-2
board.dispatch('.board .column-header', 'click')
// ничего — '.card' нет в путиРеализуй объект EventBus с методами on(event, selector, handler) и emit(event, targetPath, data). При emit должны вызываться все обработчики, у которых event совпадает и selector является подстрокой targetPath (симуляция всплытия). Также реализуй off(event, selector, handler) для удаления обработчика.
В on: this._handlers.push({ event, selector, handler }). В emit: this._handlers.filter(h => h.event === event && targetPath.includes(h.selector)).forEach(h => h.handler(data)).