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);
}
}
Важливо: скасування на клієнті не зупиняє вже розпочату роботу на сервері. Якщо запит змінює дані, сервер міг їх уже змінити.
Обидва обмежують, як часто спрацьовує функція, яку викликають дуже часто: введення тексту, 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: пізня відповідь на старий запит не має перезаписати свіжу.
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 такі обробники блокує.
Події:
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 у цьому немає потреби.
Подія в 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).
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.