Уявіть веб-сторінку без можливості взаємодії — це просто статичний документ, як роздрукована сторінка в книзі. Користувач не може натиснути кнопку, заповнити форму або навіть прокрутити контент. Саме події (events) перетворюють статичний HTML у живий, інтерактивний додаток.
Події — це сигнали від браузера, що повідомляють: "Щось сталося!". Кожна дія користувача (клік мишкою, натискання клавіші, рух курсора) або зміна стану браузера (завантаження сторінки, зміна розміру вікна) генерує відповідну подію. JavaScript дозволяє нам "слухати" ці сигнали та реагувати на них через обробники подій (event handlers).
click), ви чуєте звук (браузер надсилає сигнал) і вирішуєте, що робити — відкрити двері, подивитися у вічко або проігнорувати (обробник події).Браузерні події можна класифікувати за джерелом їх походження. Розуміння цієї класифікації допоможе вам швидше орієнтуватися в документації та писати більш структурований код.
Найпоширеніша категорія подій, пов'язана з маніпулятором-мишею:
| Подія | Опис | Коли використовувати |
|---|---|---|
click | Натискання лівої кнопки миші (або торкання на сенсорних екранах) | Основна взаємодія з кнопками та посиланнями |
dblclick | Подвійне натискання лівої кнопки | Рідко використовується в веб-інтерфейсах (зазвичай у редакторах) |
contextmenu | Натискання правої кнопки миші | Створення власних контекстних меню |
mousedown / mouseup | Натискання / відпускання кнопки миші | Drag & Drop, малювання на canvas |
mousemove | Рух миші над елементом | Відстеження позиції курсора, інтерактивні ефекти |
mouseover / mouseout | Курсор заходить на елемент / покидає елемент | Підказки (tooltips), підсвічування |
mouseenter / mouseleave | Схожі на попередні, але не спливають | Краще для делегування подій |
Всі події, пов'язані з натисканнями клавіш:
keydown — клавіша натиснута (спрацьовує постійно при утриманні)keyup — клавіша відпущенаkeypress — застаріла, не використовуйте (замість неї keydown)Критичні для обробки користувацького введення:
submit — надсилання форми <form>focus / blur — елемент отримав / втратив фокусchange — зміна значення (після втрати фокуса для <input>)input — зміна значення (в реальному часі)Пов'язані з життєвим циклом сторінки:
DOMContentLoaded — DOM повністю побудований і готовий до маніпуляційload — всі ресурси (зображення, стилі) завантаженіbeforeunload — користувач збирається покинути сторінкуunload — сторінка вивантажуєтьсяРеагують на завершення CSS-анімацій та переходів:
transitionend — CSS-перехід завершеноanimationend — CSS-анімація завершенаtouch*), перетягування (drag*), медіа (play, pause), та багато інших.Перш ніж вивчати способи роботи з подіями, важливо зрозуміти архітектуру системи подій. Це допоможе вам не лише використовувати існуючі події, а й створювати власні, розуміючи фундаментальні принципи.
Система подій браузера базується на архітектурному патерні Observer (також відомий як Pub/Sub — Publisher-Subscriber). Суть проста:
📢 Видавець (Publisher)
click при натисканні.👂 Підписник (Subscriber)
📋 Диспетчер подій (Event Manager)
Чому це важливо? Видавець не знає, хто і скільки підписників слухає його події. Підписники не знають про інших підписників. Це забезпечує слабке зчеплення (loose coupling) — компоненти незалежні один від одного.
Давайте створимо власну мініатюрну систему подій, щоб зрозуміти, як це працює всередині браузера:
// Міксин (mixin) — об'єкт з методами, які можна додати до будь-якого класу
const EventEmitter = {
/**
* Підписатися на подію
* @param {string} eventName - назва події
* @param {function} handler - функція-обробник
*/
on(eventName, handler) {
// Створюємо сховище для обробників, якщо його ще немає
if (!this._eventHandlers) {
this._eventHandlers = {}
}
// Створюємо масив для цієї події, якщо його ще немає
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = []
}
// Додаємо обробник до списку
this._eventHandlers[eventName].push(handler)
},
/**
* Відписатися від події
* @param {string} eventName - назва події
* @param {function} handler - функція-обробник для видалення
*/
off(eventName, handler) {
const handlers = this._eventHandlers?.[eventName]
if (!handlers) return // Немає обробників для цієї події
// Знаходимо та видаляємо обробник зі списку
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i, 1)
i-- // Корекція індексу після видалення
}
}
},
/**
* Викликати подію (emit/trigger/dispatch)
* @param {string} eventName - назва події
* @param {...any} args - дані для передачі обробникам
*/
emit(eventName, ...args) {
// Якщо немає обробників для цієї події — нічого не робимо
if (!this._eventHandlers?.[eventName]) {
return
}
// Викликаємо кожен обробник з переданими аргументами
this._eventHandlers[eventName].forEach((handler) => {
handler.apply(this, args)
})
},
}
Розбір коду:
on(eventName, handler) — реєструє новий обробник_eventHandlers для зберігання всіх обробниківeventName) зберігає масив обробниківhandler до цього масивуoff(eventName, handler) — видаляє обробникsplice()emit(eventName, ...args) — генерує подіюТепер використаємо наш EventEmitter для створення інтерактивного меню:
// Клас меню
class Menu {
constructor(items) {
this.items = items
this.selectedIndex = -1
}
select(index) {
if (index < 0 || index >= this.items.length) {
console.error('Невірний індекс!')
return
}
this.selectedIndex = index
const selectedItem = this.items[index]
// Генеруємо подію "select" з даними про вибраний елемент
this.emit('select', {
item: selectedItem,
index: index,
timestamp: Date.now(),
})
}
open() {
console.log('Меню відкрито')
// Генеруємо подію "open"
this.emit('open')
}
close() {
console.log('Меню закрито')
// Генеруємо подію "close"
this.emit('close')
}
}
// Додаємо можливості роботи з подіями до класу Menu
Object.assign(Menu.prototype, EventEmitter)
// Використання
const menu = new Menu(['Головна', 'Про нас', 'Контакти', 'Налаштування'])
// Підписник 1: Аналітика
menu.on('select', (data) => {
console.log(`📊 [Аналітика] Вибрано: "${data.item}" (індекс ${data.index})`)
// Тут можна відправити дані на сервер
})
// Підписник 2: Оновлення UI
menu.on('select', (data) => {
console.log(`🎨 [UI] Оновлюємо інтерфейс для пункту "${data.item}"`)
// Тут можна змінити активний клас, показати контент тощо
})
// Підписник 3: Навігація
menu.on('select', (data) => {
console.log(`🧭 [Навігація] Перехід до розділу "${data.item}"`)
// Тут можна змінити URL або завантажити контент
})
// Додаємо обробник для відкриття меню
menu.on('open', () => {
console.log('✅ Меню готове до використання')
})
// Тестуємо
menu.open()
// Виведе: Меню відкрито
// ✅ Меню готове до використання
menu.select(1)
// Виведе: 📊 [Аналітика] Вибрано: "Про нас" (індекс 1)
// 🎨 [UI] Оновлюємо інтерфейс для пункту "Про нас"
// 🧭 [Навігація] Перехід до розділу "Про нас"
menu.select(3)
// Виведе: 📊 [Аналітика] Вибрано: "Налаштування" (індекс 3)
// 🎨 [UI] Оновлюємо інтерфейс для пункту "Налаштування"
// 🧭 [Навігація] Перехід до розділу "Налаштування"
Menu не знає, як саме обробляються його події// 10 різних частин програми можуть слухати одну подію
menu.on('select', analyticsHandler)
menu.on('select', uiHandler)
menu.on('select', navigationHandler)
menu.on('select', loggingHandler)
// ... і так далі
function tempHandler(data) {
console.log('Тимчасовий обробник:', data)
}
menu.on('select', tempHandler) // Підписуємося
menu.select(0) // Обробник спрацює
menu.off('select', tempHandler) // Відписуємося
menu.select(1) // Обробник НЕ спрацює
Menu на інший, головне — зберегти подіїБраузер використовує подібний механізм, але з додатковими можливостями:
// Спрощено, як це виглядає всередині браузера:
class EventTarget {
constructor() {
this._listeners = new Map() // event -> [handlers]
}
addEventListener(type, listener, options) {
if (!this._listeners.has(type)) {
this._listeners.set(type, [])
}
this._listeners.get(type).push({ listener, options })
}
dispatchEvent(event) {
const listeners = this._listeners.get(event.type) || []
for (const { listener, options } of listeners) {
listener.call(this, event)
if (options?.once) {
this.removeEventListener(event.type, listener)
}
}
}
}
| Особливість | Наш EventEmitter | Браузер (EventTarget) |
|---|---|---|
| Підписка | on(event, handler) | addEventListener(event, handler, options) |
| Відписка | off(event, handler) | removeEventListener(event, handler) |
| Генерація | emit(event, ...args) | dispatchEvent(new Event(type)) |
| Фази подій | ❌ Немає | ✅ Capturing, Target, Bubbling |
| Об'єкт події | Довільні аргументи | Повноцінний об'єкт Event |
| Опції | ❌ Немає | ✅ once, passive, capture |
| Типові дії | ❌ Немає | ✅ preventDefault(), stopPropagation() |
button.addEventListener('click', handler), браузер:handler у внутрішній структурі данихEvent'click'Тепер, розуміючи архітектуру, ви можете створювати власні компоненти з подіями:
class FileUploader {
constructor() {
this.files = []
}
upload(file) {
// Емітуємо подію "started"
this.emit('uploadStarted', { fileName: file.name, size: file.size })
// Симуляція завантаження
setTimeout(() => {
this.files.push(file)
// Емітуємо подію "completed"
this.emit('uploadCompleted', {
fileName: file.name,
totalFiles: this.files.length,
})
}, 2000)
}
}
// Додаємо можливості подій
Object.assign(FileUploader.prototype, EventEmitter)
// Використання
const uploader = new FileUploader()
uploader.on('uploadStarted', (data) => {
console.log(`⏳ Завантаження "${data.fileName}" (${data.size} bytes)...`)
})
uploader.on('uploadCompleted', (data) => {
console.log(`✅ Файл "${data.fileName}" завантажено! Всього: ${data.totalFiles}`)
})
uploader.upload({ name: 'photo.jpg', size: 2048576 })
JavaScript пропонує три підходи до призначення обробників. Кожен має свої переваги та обмеження — важливо розуміти, коли і навіщо використовувати кожен з них.
on<event>Найпростіший, але найменш рекомендований спосіб — вказати код безпосередньо в HTML:
<button onclick="alert('Привіт!')">Натисни мене</button>
Для більших шматків коду краще винести логіку у функцію:
<script>
function handleClick() {
console.log('Кнопку натиснуто!')
}
</script>
<button onclick="handleClick()">Натисни мене</button>
Чому це працює? Браузер автоматично створює функцію-обробник з вмісту атрибута:
// Браузер перетворює onclick="handleClick()" на:
button.onclick = function (event) {
handleClick() // ваш код із атрибута
}
ONCLICK = onClick = onclick), що може заплутатиelem.on<event>Другий спосіб — використовувати властивості DOM-елемента:
<button id="myButton">Натисни мене</button>
<script>
const button = document.getElementById('myButton')
button.onclick = function () {
alert('Обробник через властивість!')
}
</script>
Ви також можете використовувати іменовані функції:
function showMessage() {
alert('Дякую за клік!')
}
button.onclick = showMessage // Без дужок!
// ❌ НЕПРАВИЛЬНО — функція виконується одразу, результат (undefined) присвоюється
button.onclick = showMessage()
// ✅ ПРАВИЛЬНО — передається посилання на функцію
button.onclick = showMessage
Обмеження: Можна призначити лише один обробник на подію. Новий обробник перезапише попередній:
button.onclick = function () {
alert('Перший')
}
button.onclick = function () {
alert('Другий')
} // Перезаписує перший!
// При кліку побачимо лише "Другий"
Щоб видалити обробник:
button.onclick = null
addEventListener (Рекомендований)Найсучасніший та найгнучкіший спосіб призначення обробників, який усуває обмеження попередніх підходів:
element.addEventListener(event, handler, [options])
Параметри:
event (string) — назва події без префікса on (наприклад, "click", "keydown")handler (function) — функція-обробникoptions (object | boolean) — додаткові налаштування:
once: true — обробник виконається один раз і автоматично видалитьсяcapture: true (або просто true) — обробник спрацює на фазі занурення замість спливанняpassive: true — обробник не викличе preventDefault() (для оптимізації на мобільних пристроях)Приклад використання:
const button = document.getElementById('myButton')
function handleClick(event) {
console.log('Кнопку натиснуто!', event)
}
// Додаємо обробник
button.addEventListener('click', handleClick)
// Можемо додати ще один — обидва виконаються!
button.addEventListener('click', function () {
console.log('Другий обробник!')
})
Видалення обробника:
// Видалення вимагає передачі ТОЧНО тієї ж функції
button.removeEventListener('click', handleClick)
// ❌ Не працюватиме — це дві різні функції!
button.addEventListener('click', () => alert('Привіт'))
button.removeEventListener('click', () => alert('Привіт'))
// ✅ Правильно — зберігаємо посилання на функцію
const handler = () => alert('Привіт')
button.addEventListener('click', handler)
button.removeEventListener('click', handler) // Працює!
Основна перевага — можливість призначити кілька обробників на одну подію:
<button id="btn">Багатофункціональна кнопка</button>
<script>
const btn = document.getElementById('btn')
// Перший обробник — аналітика
btn.addEventListener('click', () => {
console.log('Відправлено подію в аналітику')
})
// Другий обробник — зміна UI
btn.addEventListener('click', () => {
btn.textContent = 'Натиснуто!'
})
// Третій обробник — запит на сервер
btn.addEventListener('click', () => {
fetch('/api/button-clicked').then((res) => res.json())
})
// Всі три виконаються послідовно
</script>
addEventListener, наприклад:// ❌ Не працює
document.onDOMContentLoaded = function () {
console.log('DOM готовий')
}
// ✅ Працює
document.addEventListener('DOMContentLoaded', function () {
console.log('DOM готовий')
})
| Критерій | HTML-атрибут | DOM-властивість | addEventListener |
|---|---|---|---|
| Читабельність | Низька | Середня | Висока |
| Розділення HTML/JS | ❌ Ні | ✅ Так | ✅ Так |
| Кількість обробників | 1 | 1 | Необмежено |
| Видалення обробника | Складно | elem.onclick = null | removeEventListener() |
| Контроль фаз (capture/bubble) | ❌ Ні | ❌ Ні | ✅ Так |
| Додаткові опції | ❌ Ні | ❌ Ні | ✅ Так (once, passive) |
| Використання в сучасних проєктах | ❌ Не рекомендується | 🟡 Іноді | ✅ Так |
::
::
Коли відбувається подія, браузер створює об'єкт події (event object) і передає його першим параметром у функцію-обробник. Цей об'єкт містить всю інформацію про подію.
<button id="infoButton">Натисни мене</button>
<script>
const button = document.getElementById('infoButton')
button.addEventListener('click', function (event) {
console.log('Тип події:', event.type) // "click"
console.log('Елемент-ціль:', event.target) // <button id="infoButton">
console.log('Елемент-обробник:', event.currentTarget) // <button id="infoButton">
console.log('Координати кліка:', event.clientX, event.clientY)
console.log('Натиснута Ctrl?', event.ctrlKey)
console.log('Мітка часу:', event.timeStamp)
})
</script>
"click", "keydown")this у звичайних функціях)key — символ, code — фізична клавішаtrue — подія викликана користувачем, false — згенерована через JavaScripttarget та currentTargetЦе одна з найбільш заплутаних концепцій для початківців. Розберемо на прикладі:
<div id="outer" style="padding: 50px; background: lightblue;">
Зовнішній div
<button id="inner">Внутрішня кнопка</button>
</div>
<script>
const outer = document.getElementById('outer')
outer.addEventListener('click', function (event) {
console.log('event.target:', event.target.id) // Елемент, на який клікнули
console.log('event.currentTarget:', event.currentTarget.id) // Завжди "outer"
console.log('this:', this.id) // Також "outer"
})
</script>
Що станеться:
event.target = button#inner, event.currentTarget = div#outerevent.target = div#outer, event.currentTarget = div#outerthisУ звичайній функції-обробнику this завжди вказує на елемент, до якого призначений обробник:
button.onclick = function () {
console.log(this) // <button>
this.style.backgroundColor = 'red' // Змінюємо фон кнопки
}
this!// ❌ this === window (або undefined в strict mode)
button.onclick = () => {
console.log(this) // НЕ button!
}
// ✅ Використовуйте event.currentTarget
button.onclick = (event) => {
console.log(event.currentTarget) // <button>
}
handleEventНезвичний, але потужний паттерн — передача об'єкта з методом handleEvent замість функції:
<button id="menu">Відкрити меню</button>
<script>
class Menu {
handleEvent(event) {
// Автоматичний роутинг подій до методів класу
const method = 'on' + event.type[0].toUpperCase() + event.type.slice(1)
this[method](event)
}
onClick(event) {
console.log('Меню відкрито!')
this.showMenu()
}
onMouseover(event) {
event.target.style.backgroundColor = '#f0f0f0'
}
onMouseout(event) {
event.target.style.backgroundColor = ''
}
showMenu() {
alert('Показуємо меню...')
}
}
const menu = new Menu()
const btn = document.getElementById('menu')
// Передаємо об'єкт замість функції
btn.addEventListener('click', menu)
btn.addEventListener('mouseover', menu)
btn.addEventListener('mouseout', menu)
</script>
Переваги підходу:
this автоматично вказує на екземпляр класуДавайте створимо практичний приклад, який демонструє всі описані концепції:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Лічильник кліків</title>
<style>
.counter {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 50px auto;
padding: 30px;
border: 2px solid #3b82f6;
border-radius: 10px;
text-align: center;
}
.count {
font-size: 48px;
font-weight: bold;
color: #3b82f6;
margin: 20px 0;
}
button {
font-size: 18px;
padding: 10px 20px;
margin: 5px;
cursor: pointer;
border: none;
border-radius: 5px;
background: #3b82f6;
color: white;
transition: background 0.3s;
}
button:hover {
background: #2563eb;
}
button.reset {
background: #ef4444;
}
button.reset:hover {
background: #dc2626;
}
.info {
margin-top: 20px;
padding: 15px;
background: #f1f5f9;
border-radius: 5px;
font-size: 14px;
color: #475569;
}
</style>
</head>
<body>
<div class="counter">
<h1>Лічильник подій</h1>
<div class="count" id="count">0</div>
<button id="incrementBtn">➕ Збільшити</button>
<button id="decrementBtn">➖ Зменшити</button>
<button id="resetBtn" class="reset">🔄 Скинути</button>
<div class="info" id="info">Натисніть кнопку для зміни лічильника</div>
</div>
<script>
// Знаходимо елементи
const countDisplay = document.getElementById('count')
const incrementBtn = document.getElementById('incrementBtn')
const decrementBtn = document.getElementById('decrementBtn')
const resetBtn = document.getElementById('resetBtn')
const info = document.getElementById('info')
// Стан лічильника
let count = 0
// Функція оновлення відображення
function updateDisplay() {
countDisplay.textContent = count
}
// Функція показу інформації про подію
function showEventInfo(event) {
info.innerHTML = `
<strong>Подія:</strong> ${event.type}<br>
<strong>Елемент:</strong> ${event.target.textContent}<br>
<strong>Час:</strong> ${new Date().toLocaleTimeString()}<br>
<strong>Координати:</strong> (${event.clientX}, ${event.clientY})
`
}
// Обробники подій
incrementBtn.addEventListener('click', function (event) {
count++
updateDisplay()
showEventInfo(event)
})
decrementBtn.addEventListener('click', function (event) {
count--
updateDisplay()
showEventInfo(event)
})
// Приклад використання опції { once: true }
resetBtn.addEventListener('click', function (event) {
count = 0
updateDisplay()
showEventInfo(event)
// Додаємо тимчасовий обробник, який спрацює один раз
this.addEventListener(
'mouseover',
function tempHandler(e) {
info.innerHTML += '<br><em>Ефект скидання активний!</em>'
},
{ once: true },
)
})
// Додаємо ефект наведення для всіх кнопок
const buttons = document.querySelectorAll('button')
buttons.forEach((button) => {
button.addEventListener('mouseenter', function () {
this.style.transform = 'scale(1.05)'
})
button.addEventListener('mouseleave', function () {
this.style.transform = 'scale(1)'
})
})
</script>
</body>
</html>
Розбір коду:
getElementById()count для зберігання поточного значенняupdateDisplay() оновлює текстовий вміст елементаshowEventInfo() демонструє властивості об'єкта подіїevent{ once: true } — обробник виконається один раз❌ Помилка #1: Виклик функції замість передачі посилання
// Неправильно
button.onclick = handleClick() // Викликає зараз!
// Правильно
button.onclick = handleClick // Передає посилання
❌ Помилка #2: Використання setAttribute для подій
// Не працює — функція перетворюється на рядок
element.setAttribute('onclick', function () {
alert(1)
})
// Правильно
element.onclick = function () {
alert(1)
}
❌ Помилка #3: Чутливість до регістру
element.ONCLICK = handler // Не працює!
element.onclick = handler // Працює ✅
// DOM-властивості чутливі до регістру!
❌ Помилка #4: Неможливість видалити анонімну функція
// Неможливо видалити пізніше
elem.addEventListener('click', () => alert('Hi'))
// Правильно — зберігаємо посилання
const handler = () => alert('Hi')
elem.addEventListener('click', handler)
elem.removeEventListener('click', handler)
Події — це мова спілкування між користувачем та вашим додатком. Кожна дія генерує сигнал, який можна обробити.
Кожен обробник отримує об'єкт event з повною інформацією про подію: тип, цільовий елемент, координати, мітка часу тощо.
event.target — елемент, де відбулася подіяevent.currentTarget (або this) — елемент з обробникомaddEventListener дозволяє кілька обробників, DOM-властивості — лише одинДалі ви вивчите механізм спливання (bubbling) та занурення (capturing), які розкривають повну міць системи подій браузера.
mouseover) показує кнопку "Купити"dblclick) скидає станaddEventListener для всіх подій на одному елементі.Ctrl+S виводить alert "Збережено!" (замість стандартного збереження сторінки)Escape ховає модальне вікноevent.ctrlKey, event.key та event.preventDefault().<ul>, який:<li> клікнули через event.target📚 MDN Web Docs
Introduction to events — детальний посібник від Mozilla
EventTarget.addEventListener() — повна специфікація методу
🎓 Специфікація W3C
🛠️ JavaScript.info
Наступна стаття: Бульбашковий механізм (спливання та занурення)