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

JavaScript: DOM, події й API браузера

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

Пошук і зміна елементів, спливання й делегування подій, власні події, спостерігачі, FormData, історія й адреса, fetch - питання всіх рівнів, від junior до senior.

За спробу
20
У пулі
44
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

26 питань

Сам Promise скасувати не можна - лише проігнорувати його результат. Справжнє скасування роблять через AbortController: операція отримує signal і сама стежить, чи його не скасували.

const controller = new AbortController();

fetch('/api/search?q=laravel', { signal: controller.signal })
  .then((r) => r.json())
  .then(render)
  .catch((error) => {
    if (error.name === 'AbortError') return; // скасовано свідомо - не помилка
    showError(error);
  });

controller.abort(); // запит перервано, з'єднання закрито

Типові сценарії:

  • Пошук під час набору: кожна нова літера скасовує попередній запит, щоб старіша відповідь не перезаписала новішу (класична гонка).
  • Компонент зник (React-ефект, Vue onUnmounted) - скасувати його запити й підписки.
  • Тайм-аут: fetch(url, { signal: AbortSignal.timeout(5000) }).
  • Кілька причин скасування одразу: AbortSignal.any([userSignal, AbortSignal.timeout(5000)]).

Сигнал працює не лише з fetch: addEventListener(type, fn, { signal }) знімає обробник при скасуванні, а у власних функціях можна перевіряти signal.aborted чи слухати подію abort:

async function processAll(items, signal) {
  for (const item of items) {
    signal.throwIfAborted();
    await process(item);
  }
}

Важливо: скасування на клієнті не зупиняє вже розпочату роботу на сервері. Якщо запит змінює дані, сервер міг їх уже змінити.

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

Обидва обмежують, як часто спрацьовує функція, яку викликають дуже часто: введення тексту, scroll, resize, рух миші.

  • Debounce - виконати один раз, коли виклики припинилися на заданий час. Пошук під час набору: запит іде, коли користувач перестав друкувати на 300 мс.
  • Throttle - виконувати не частіше ніж раз на заданий проміжок, поки виклики тривають. Позиція скролу для прогрес-бару: раз на 100 мс, а не 60 разів на секунду.

Обидва - класичний приклад замикань: таймер і час останнього виклику живуть у зовнішній функції.

function debounce(fn, wait) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

function throttle(fn, wait) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}

input.addEventListener('input', debounce((e) => search(e.target.value), 300));

Що питають на старших рівнях:

  • Leading / trailing: чи виконувати функцію на початку серії, наприкінці чи обидва рази. Throttle вище втрачає останній виклик серії - для прогрес-бару це помилка, бо фінальна позиція не потрапить.
  • Скасування: debounce у компоненті треба вміти скасувати при його знищенні, інакше колбек спрацює вже після того.
  • requestAnimationFrame - природний «throttle» для всього, що малює: не частіше за кадр.

Для debounce пошукових запитів додають ще AbortController: пізня відповідь на старий запит не має перезаписати свіжу.

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

element.onclick = fn - лише один обробник: наступне присвоєння перезаписує попередній. addEventListener додає скільки завгодно обробників на одну подію й має налаштування.

button.onclick = () => track('click');
button.onclick = () => openModal();   // track більше не спрацює

button.addEventListener('click', () => track('click'));
button.addEventListener('click', () => openModal()); // спрацюють обидва

Опції addEventListener:

  • once: true - обробник автоматично знімається після першого спрацювання.
  • passive: true - обіцянка не викликати preventDefault(). Для touchstart/wheel це дозволяє браузеру прокручувати сторінку плавно, не чекаючи обробника.
  • capture: true - обробник спрацює на фазі перехоплення, ще до цільового елемента.
  • signal - зняти обробник через AbortController, зручно для кількох обробників одразу.

Зняти обробник можна лише тим самим посиланням на функцію:

function onResize() { /* ... */ }
window.addEventListener('resize', onResize);
window.removeEventListener('resize', onResize);   // працює

window.addEventListener('resize', () => {});
window.removeEventListener('resize', () => {});   // нічого не зніме: інша функція

Атрибути в HTML (<button onclick="...">) - найгірший варіант: код змішано з розміткою, а суворий Content Security Policy такі обробники блокує.

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

Події:

  • DOMContentLoaded - HTML повністю розібрано, DOM готовий. Зображення, стилі (крім тих, що блокують скрипти) й iframe ще можуть вантажитися. Тут зазвичай ініціалізують скрипти.
  • load (на window) - завантажено все: зображення, шрифти, iframe. Відбувається пізніше, часто значно.

Звичайний <script src> у <head> зупиняє розбір HTML, доки скрипт не завантажиться й не виконається. Сторінка «біла», а скрипт ще й не бачить елементи нижче.

  • defer - завантажити паралельно з розбором, а виконати після розбору HTML, перед DOMContentLoaded, у порядку появи в документі.
  • async - завантажити паралельно й виконати, щойно завантажився, у довільному порядку, перериваючи розбір.
<script defer src="/app.js"></script>        <!-- основний код, залежить від DOM -->
<script async src="/analytics.js"></script>  <!-- незалежний, порядок не важливий -->
<script type="module" src="/main.js"></script> <!-- модулі поводяться як defer -->

Правило: код застосунку - defer (або type="module"), незалежні сторонні скрипти (аналітика, віджети) - async. Розміщувати скрипти в кінці <body> - старий спосіб досягти схожого ефекту; з defer у цьому немає потреби.

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

Подія в 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).

Докладніше в документації: Спливання подій

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.