Ты разрабатываешь Trello. Пользователь кликает «Добавить карточку», вводит текст, нажимает Enter — карточка появляется на доске. Эти взаимодействия — клики, нажатия, ввод — браузер оборачивает в события. JavaScript реагирует на них через обработчики.
const addBtn = document.querySelector('#add-card-btn')
addBtn.addEventListener('click', function(event) {
console.log('Кликнули по кнопке')
console.log(event.type) // 'click'
console.log(event.target) // элемент, на котором произошло событие
})
// Стрелочная функция — короче, но this будет другим
addBtn.addEventListener('click', (event) => {
createCard(event.target.dataset.listId)
})form.addEventListener('submit', (event) => {
event.preventDefault() // отменяет отправку формы на сервер (перезагрузку)
event.stopPropagation() // останавливает всплытие к родителям
const formData = new FormData(event.target)
const title = formData.get('title')
createCard(title)
})Ключевые свойства:
'Enter', 'Escape', 'ArrowUp'Событие сначала срабатывает на целевом элементе, потом всплывает к родителям:
// Клик на <button> внутри <li> внутри <ul>:
// button → li → ul → body → html → documentЭто поведение — основа делегирования событий.
Один обработчик на родителе вместо обработчика на каждом дочернем элементе:
// Плохо: N карточек = N обработчиков = расход памяти
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', handleCardClick)
})
// Хорошо: один обработчик на весь список
const board = document.querySelector('#board')
board.addEventListener('click', (event) => {
// closest() ищет ближайший предок с нужным селектором
const btn = event.target.closest('[data-action]')
if (!btn) return // клик не по кнопке с data-action — игнорируем
const action = btn.dataset.action // 'delete', 'edit', 'move'
const cardId = btn.closest('.card').dataset.id
handleCardAction(action, parseInt(cardId))
})Преимущества делегирования:
1. Один обработчик вместо тысячи — меньше памяти
2. Работает для динамически добавленных элементов
3. Легко управлять — убрать один обработчик, а не тысячу
// Живой поиск — каждый введённый символ
searchInput.addEventListener('input', e => filterResults(e.target.value))
// Горячие клавиши
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal()
if (e.key === 'Enter' && e.ctrlKey) submitForm()
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault() // не сохранять страницу!
saveDocument()
}
})
// Инициализация приложения
document.addEventListener('DOMContentLoaded', () => initApp())1. Забыли preventDefault() на форме — страница перезагружается:
// Сломано:
form.addEventListener('submit', (e) => {
// нет e.preventDefault() — форма отправится, страница перезагрузится!
const data = collectFormData() // не успеет выполниться
})
// Исправлено:
form.addEventListener('submit', (e) => {
e.preventDefault() // блокируем стандартное поведение браузера
const data = collectFormData()
sendToServer(data)
})2. Используют onclick вместо addEventListener — теряют предыдущие обработчики:
// Сломано — второй вызов затирает первый:
btn.onclick = showTooltip
btn.onclick = openModal // showTooltip больше не сработает!
// Исправлено:
btn.addEventListener('click', showTooltip)
btn.addEventListener('click', openModal) // оба сработают3. Не убирают обработчик при удалении элемента — утечка памяти:
// Лучше — автоудаление после первого срабатывания:
modal.addEventListener('click', closeOnBackdrop, { once: true })onClick, onChange, onSubmit — синтетические события поверх addEventListenerwindow.addEventListener('scroll', loadMore)Симуляция делегирования событий: доска задач Trello-стиль
// Симуляция системы событий без реального DOM
// В браузере: board.addEventListener('click', e => dispatch(e.target.dataset))
const board = {
cards: [
{ id: 1, title: 'Изучить промисы', done: false, priority: 'high' },
{ id: 2, title: 'Написать тесты', done: false, priority: 'medium' },
{ id: 3, title: 'Сделать код-ревью', done: true, priority: 'low' },
],
}
// Единый диспетчер — один обработчик вместо N кнопок на N карточках
function dispatch(action, cardId) {
const card = board.cards.find(c => c.id === cardId)
if (!card) { console.log('Карточка не найдена:', cardId); return }
switch (action) {
case 'toggle':
card.done = !card.done
console.log(`[${card.done ? 'x' : ' '}] ${card.title}`)
break
case 'delete': {
const idx = board.cards.indexOf(card)
board.cards.splice(idx, 1)
console.log(`Удалено: "${card.title}"`)
break
}
case 'priority': {
const levels = ['low', 'medium', 'high']
const next = levels[(levels.indexOf(card.priority) + 1) % levels.length]
card.priority = next
console.log(`"${card.title}" → приоритет: ${next}`)
break
}
default:
console.log('Неизвестное действие:', action)
}
}
// В браузере это была бы HTML-разметка:
// <ul id="board">
// <li class="card" data-id="1">
// <button data-action="toggle">✓</button>
// <button data-action="delete">✕</button>
// <button data-action="priority">↑</button>
// </li>
// </ul>
// И ОДИН обработчик на всю доску
console.log('=== Начальное состояние ===')
board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))
console.log('\n=== Симуляция кликов ===')
dispatch('toggle', 1) // отмечаем выполненной
dispatch('priority', 2) // повышаем приоритет
dispatch('delete', 3) // удаляем
console.log('\n=== После изменений ===')
board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))Ты разрабатываешь Trello. Пользователь кликает «Добавить карточку», вводит текст, нажимает Enter — карточка появляется на доске. Эти взаимодействия — клики, нажатия, ввод — браузер оборачивает в события. JavaScript реагирует на них через обработчики.
const addBtn = document.querySelector('#add-card-btn')
addBtn.addEventListener('click', function(event) {
console.log('Кликнули по кнопке')
console.log(event.type) // 'click'
console.log(event.target) // элемент, на котором произошло событие
})
// Стрелочная функция — короче, но this будет другим
addBtn.addEventListener('click', (event) => {
createCard(event.target.dataset.listId)
})form.addEventListener('submit', (event) => {
event.preventDefault() // отменяет отправку формы на сервер (перезагрузку)
event.stopPropagation() // останавливает всплытие к родителям
const formData = new FormData(event.target)
const title = formData.get('title')
createCard(title)
})Ключевые свойства:
'Enter', 'Escape', 'ArrowUp'Событие сначала срабатывает на целевом элементе, потом всплывает к родителям:
// Клик на <button> внутри <li> внутри <ul>:
// button → li → ul → body → html → documentЭто поведение — основа делегирования событий.
Один обработчик на родителе вместо обработчика на каждом дочернем элементе:
// Плохо: N карточек = N обработчиков = расход памяти
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', handleCardClick)
})
// Хорошо: один обработчик на весь список
const board = document.querySelector('#board')
board.addEventListener('click', (event) => {
// closest() ищет ближайший предок с нужным селектором
const btn = event.target.closest('[data-action]')
if (!btn) return // клик не по кнопке с data-action — игнорируем
const action = btn.dataset.action // 'delete', 'edit', 'move'
const cardId = btn.closest('.card').dataset.id
handleCardAction(action, parseInt(cardId))
})Преимущества делегирования:
1. Один обработчик вместо тысячи — меньше памяти
2. Работает для динамически добавленных элементов
3. Легко управлять — убрать один обработчик, а не тысячу
// Живой поиск — каждый введённый символ
searchInput.addEventListener('input', e => filterResults(e.target.value))
// Горячие клавиши
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal()
if (e.key === 'Enter' && e.ctrlKey) submitForm()
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault() // не сохранять страницу!
saveDocument()
}
})
// Инициализация приложения
document.addEventListener('DOMContentLoaded', () => initApp())1. Забыли preventDefault() на форме — страница перезагружается:
// Сломано:
form.addEventListener('submit', (e) => {
// нет e.preventDefault() — форма отправится, страница перезагрузится!
const data = collectFormData() // не успеет выполниться
})
// Исправлено:
form.addEventListener('submit', (e) => {
e.preventDefault() // блокируем стандартное поведение браузера
const data = collectFormData()
sendToServer(data)
})2. Используют onclick вместо addEventListener — теряют предыдущие обработчики:
// Сломано — второй вызов затирает первый:
btn.onclick = showTooltip
btn.onclick = openModal // showTooltip больше не сработает!
// Исправлено:
btn.addEventListener('click', showTooltip)
btn.addEventListener('click', openModal) // оба сработают3. Не убирают обработчик при удалении элемента — утечка памяти:
// Лучше — автоудаление после первого срабатывания:
modal.addEventListener('click', closeOnBackdrop, { once: true })onClick, onChange, onSubmit — синтетические события поверх addEventListenerwindow.addEventListener('scroll', loadMore)Симуляция делегирования событий: доска задач Trello-стиль
// Симуляция системы событий без реального DOM
// В браузере: board.addEventListener('click', e => dispatch(e.target.dataset))
const board = {
cards: [
{ id: 1, title: 'Изучить промисы', done: false, priority: 'high' },
{ id: 2, title: 'Написать тесты', done: false, priority: 'medium' },
{ id: 3, title: 'Сделать код-ревью', done: true, priority: 'low' },
],
}
// Единый диспетчер — один обработчик вместо N кнопок на N карточках
function dispatch(action, cardId) {
const card = board.cards.find(c => c.id === cardId)
if (!card) { console.log('Карточка не найдена:', cardId); return }
switch (action) {
case 'toggle':
card.done = !card.done
console.log(`[${card.done ? 'x' : ' '}] ${card.title}`)
break
case 'delete': {
const idx = board.cards.indexOf(card)
board.cards.splice(idx, 1)
console.log(`Удалено: "${card.title}"`)
break
}
case 'priority': {
const levels = ['low', 'medium', 'high']
const next = levels[(levels.indexOf(card.priority) + 1) % levels.length]
card.priority = next
console.log(`"${card.title}" → приоритет: ${next}`)
break
}
default:
console.log('Неизвестное действие:', action)
}
}
// В браузере это была бы HTML-разметка:
// <ul id="board">
// <li class="card" data-id="1">
// <button data-action="toggle">✓</button>
// <button data-action="delete">✕</button>
// <button data-action="priority">↑</button>
// </li>
// </ul>
// И ОДИН обработчик на всю доску
console.log('=== Начальное состояние ===')
board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))
console.log('\n=== Симуляция кликов ===')
dispatch('toggle', 1) // отмечаем выполненной
dispatch('priority', 2) // повышаем приоритет
dispatch('delete', 3) // удаляем
console.log('\n=== После изменений ===')
board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))Ты разрабатываешь ленту постов социальной сети (Instagram-стиль). Симулируй делегирование событий через JavaScript без DOM. Создай массив постов и функцию `handlePostAction(postId, action)`, обрабатывающую три действия: - `'like'` — увеличивает счётчик лайков, выводит новое значение - `'bookmark'` — переключает `bookmarked` (true/false), выводит статус - `'delete'` — удаляет пост из массива, выводит подтверждение Покажи, что один обработчик заменяет множество индивидуальных.
post.likes++ для лайка. post.bookmarked = !post.bookmarked для закладки. Для удаления: const idx = posts.findIndex(p => p.id === postId); posts.splice(idx, 1). Вся идея: один handlePostAction вместо обработчика на каждом посте.