Виконання JavaScript та event loop
Зміст
«Клік не спрацював», «елемент є в DOM, а тест падає», «той самий тест то зелений, то червоний» — три типові скарги на автотести, і за всіма трьома стоїть одне: браузер виконує JavaScript не так, як здається з коду тесту. Сторінка робить одну річ за раз і має власний порядок, у якому бере відкладену роботу. Поки цього порядку немає в голові, очікування в тестах виглядають забобоном, а фіксована пауза — розумним компромісом.
Це канонічна глава теми: тут повний виклад моделі виконання, інші глави розділу посилаються сюди. Синтаксис і async/await як мова — тема розділу про JavaScript і TypeScript; тут лише модель виконання.
Один потік і стек викликів
JavaScript у браузері виконується в одному потоці: у кожен момент часу (JavaScript engine) виконує рівно один фрагмент коду. Найменша одиниця виконання — контекст виконання, він же кадр стека (frame): виклик функції кладе кадр на стек викликів (call stack), завершення знімає його, і кожна порція роботи починається та закінчується порожнім стеком.
Звідси головна властивість моделі — run-to-completion: кожне завдання обробляється повністю, перш ніж почнеться будь-яке інше, і середовище не витісняє JS-код примусово, як це роблять потокові мови. Поки стек не порожній, потік зайнятий — ні обробки кліків, ні анімацій, ні перемальовування, ні відкладених .
Ось звідки береться «вкладка не відповідає»: важкі обчислення в циклі, синхронний розбір великого JSON, масові операції над деревом — одна , під час якої сторінка мертва. Модель обіцяє неблокуюче виконання, але обіцянка тримається лише на асинхронних API: легасі-винятки на кшталт alert() чи синхронного XHR блокують потік повністю. Лікується це двома способами — розбити роботу на частини або винести обчислення у Web Worker, окремий потік без доступу до DOM.
Наша практика (не канон). Джерела реєстру цього не формулюють. Один потік існує через DOM: два потоки над одним деревом вимагали б блокувань. Stack trace — знімок стека в момент збою, читається згори вниз: угорі функція, де впало, нижче — ті, хто її викликав; RangeError: Maximum call stack size exceeded означає, що рекурсія кладе кадри швидше, ніж вони знімаються. І типовий симптом: «клік не спрацював» частіше означає зайнятий потік, а не поганий .
Подієвий цикл: хто вирішує, коли виконати відкладене
(event loop) — механізм, яким браузер узгоджує події, дії користувача, скрипти, рендеринг і мережу; потоком операційної системи він не є — кілька циклів можуть кооперативно жити в одному потоці. Спрощена логіка така: виконати весь синхронний код → коли стек порожній, узяти наступне завдання з черги й покласти на стек → повторити. Нові завдання додаються в кінець черги.
Ключове: таймери, ввід-вивід і події лише додають завдання в чергу — момент виконання визначає цикл. Колбек асинхронної дії не виконується миттєво, він стає завданням; звідси головний наслідок: асинхронний колбек ніколи не перерве синхронний код, що вже виконується.
Черг завдань буває кілька, і формально це навіть не черги, а множини: цикл бере перше придатне до виконання завдання, а не буквально перше. Кожне завдання належить своєму джерелу (події, парсер, таймери), а джерело прив'язане до конкретної черги — це зберігає порядок усередині джерела. Рендеринг же відбувається не після кожного завдання, а в «нагоду для рендерингу», яку визначають частота оновлення екрана й браузера.
Задачі і мікрозадачі: чому порядок саме такий
Черга не одна, і це те місце, де плутаються найчастіше. Черга мікрозадач — не одна з черг задач, а окрема сутність: специфікація каже прямо («The microtask queue is not a task queue»). Розподіл такий:
- Задачі (task) — колбеки
setTimeoutіsetInterval, події DOM, події XHR. - Мікрозадачі (microtask) — реакції промісів,
queueMicrotask, колбекиMutationObserver.
Правило, яке пояснює майже всю поведінку асинхронного JS: після кожного виконаного завдання цикл проходить контрольну точку мікрозадач і спорожнює чергу мікрозадач повністю — включно з тими, що додалися під час її виконання, — і лише потім бере наступну задачу. Тому мікрозадачі мають вищий пріоритет; а код, який безперервно додає нові мікрозадачі, не дає циклу дійти ні до наступної задачі, ні до нагоди перемалювати сторінку.
console.log('1: синхронний старт');
setTimeout(() => console.log('2: setTimeout — задача'), 0);
Promise.resolve().then(() => console.log('3: promise — мікрозадача'));
console.log('4: синхронний кінець');
1: синхронний старт
4: синхронний кінець
3: promise — мікрозадача
2: setTimeout — задача
Спершу виконується весь синхронний код (1 і 4), стек порожніє. Далі цикл спорожнює чергу мікрозадач — тому 3 випереджає 2, хоч setTimeout записаний вище. І лише потім береться задача.
| Аспект | Мікрозадачі | Задачі |
|---|---|---|
| Хто потрапляє | реакції промісів, queueMicrotask, MutationObserver | setTimeout, setInterval, події DOM, події XHR |
| Коли виконуються | уся черга — одразу після поточного завдання | по одній за прохід циклу |
| Пріоритет | вищий | нижчий |
Про термін. Пару «мікрозадача — макрозадача» вживають у побуті, але канонічні джерела її не знають: слова macrotask немає ні в MDN, ні в HTML Living Standard — MDN каже «HTML event loops split jobs into two categories: tasks and microtasks». Тому «макрозадача» лишається жаргоном: розпізнавати на слух, самому не вживати.
Проміси, async/await і мережа в цій моделі
Проміс (promise) має рівно три взаємовиключні стани — pending, fulfilled, rejected, — і це нормативна вимога мови; завершений (settled) проміс уже не змінюється, а повторний resolve чи reject не робить нічого. Ланцюг промісів зʼявився як заміна вкладеним колбекам («callback hell», «піраміда приреченості»): кроки стають лінійними, а помилки збирає один catch. Комбінатори Promise.all, Promise.allSettled, Promise.race і Promise.any узгоджують кілька операцій; повний розбір разом із синтаксисом — у розділі про JavaScript і TypeScript. Для подієвого циклу важать три речі.
Реакції промісів — мікрозадачі. Функція, передана в then(), ніколи не викликається синхронно, навіть на вже розвʼязаному промісі: вона йде в чергу мікрозадач і виконується, коли стек порожній, перед поверненням керування в цикл.
await не блокує потік. Асинхронна функція завжди повертає проміс; код до першого await включно виконується синхронно, а код після кожного await — це фактично колбек .then, тобто продовження їде в чергу мікрозадач. Це пауза для однієї функції, а не заморозка сторінки.
Мережа приходить задачею, а обробляється мікрозадачею. Стандарт формулює це загальним правилом: коли алгоритм тягне ресурс неблокуючим способом, обробка отриманого «is performed by a task» — саме ця задача й резолвить проміс запиту. Реакції .then/.catch виконуються вже мікрозадачами, і це не домовленість реалізацій: планування промісних операцій HTML кладе саме в чергу мікрозадач («HTML schedules these operations in the microtask queue»). Старий XHR.onload мікрозадачного етапу не має — він лишається задачею.
Одну модальність тут варто взяти точно, бо її часто переказують сильнішою, ніж вона є. Стандарт каже, що диспетчеризація події на цілі «often done by a dedicated task», і одразу застерігає: «Not all events are dispatched using the task queue; many are dispatched during other tasks». Тобто коректне формулювання — «обробник події лишається задачею, а не мікрозадачею», а не «подія завжди створює окрему задачу».
Таймери: чому нуль не означає нуль
setTimeout(fn, 0) не виконує fn негайно з двох незалежних причин.
Перша — черга. Колбек таймера потрапляє в чергу як глобальна задача з джерела задач таймерів. Навіть із нульовою він чекає, поки завершиться синхронний код і спорожниться черга мікрозадач. «Нуль» означає «якнайшвидше після того, як звільниться потік», а не «зараз».
Друга — затримка це мінімум, а не розклад. Специфікація заявляє прямо: «This API does not guarantee that timers will run exactly on schedule. Delays due to CPU load, other tasks, etc, are to be expected». Відʼємна затримка нормалізується в нуль.
Для вкладених таймерів додається жорсткий мінімум: якщо рівень вкладеності більший за 5, а задана затримка менша за 4, затримка стає 4 мс; примітка стандарту каже те саме словами — після пʼяти таких вкладень інтервал примусово стає не меншим за чотири мілісекунди. Умова саме «більший за 5», тож clamp вмикається з шостого рівня. Дві деталі відрізняють вивчене від зрозумілого: рівень рахується для вкладених викликів самого алгоритму, тож повторюваний setInterval набирає рівні без ручного вкладання; а 4 мс — нижня межа, а не обіцяний період, бо браузеру окремо дозволено доповнити затримку на визначений реалізацією час, наприклад заради економії енергії.
Практичний висновок: setTimeout(fn, 0) — спосіб відкласти роботу на наступний прохід циклу, а не спосіб виміряти час.
AJAX і fetch: звідки береться проміжок
AJAX — підхід, за якого сторінка обмінюється даними із сервером у фоні, без повного перезавантаження: сторінка не «моргає», а оновлюється лише потрібна частина DOM. Технічно це роблять двома способами — старим XMLHttpRequest і сучасним Fetch API, побудованим на промісах, а не колбеках.
Для тестів тут дві дорогі пастки. Перша: fetch() не реджектиться на HTTP-помилці. Відповідь 404 чи 500 дає успішний проміс із обʼєктом Response, тож статус перевіряють вручну (response.ok істинний лише для діапазону 2xx). Реджект настає «on some errors, such as a network error or a bad scheme» — перелік у джерелі навмисно відкритий. Друга: у XMLHttpRequest стан 4 DONE означає «операція завершилась» — успішно або з помилкою, тож сам по собі readyState === 4 про успіх не свідчить.
Обидва механізми йдуть через мережевий стек браузера, тому інструменти автоматизації ловлять їх однаково — і щоб дочекатися конкретної відповіді, і щоб її підмінити. Це тема глав Перехоплення й мокання мережі та Асинхронне завантаження: SPA/MPA, CSR/SSR; статуси відповідей — HTTP статус-коди.
Події DOM: порядок обробників
Подія проходить дерево трьома фазами: захоплення (capturing) — до цілі, фаза цілі, спливання (bubbling) — після цілі; поточну фазу видно у властивості eventPhase. Порядок точний: спершу в порядку дерева спрацьовують слухачі, зареєстровані з capture: true, а потім, якщо подія спливає, — у зворотному порядку решта. За замовчуванням обробник слухає саме спливання.
Порядок ламають три методи, і плутати їх не можна. stopPropagation() не дає події дістатися інших обʼєктів дерева, але стандартної дії браузера не скасовує. stopImmediatePropagation() додатково відсікає решту слухачів на тому самому обʼєкті. preventDefault() скасовує стандартну дію (лише для подій із cancelable: true) і поширення не спиняє; успіх видно через defaultPrevented.
Спливають не всі події. blur не спливає, а focusout — спливає; порядок теж нормативний: focus спрацьовує перед focusin, blur — перед focusout. Практичний наслідок: делегування (event delegation) на групу полів працює через focusout і не працює через blur. Саме делегування прямо випливає зі спливання: один обробник на спільному предку обслуговує всі дочірні елементи, зокрема ті, що зʼявляться згодом, а e.target — найглибший елемент, по якому реально клікнули.
У часі ж обробники живуть усередині завдання: воно виконується до завершення й нікого не перериває. Тому «порядок обробників» має два виміри — у дереві (фази) і в часі (черги). Канон подій — глава DOM, селектори та події.
DOM і головний потік: що ділиться, а що виконується поза ним
DOM — не текст і не файл, а структура в памʼяті браузера: розібравши HTML, браузер будує дерево й показує сторінку вже з нього, а скрипти змінюють це дерево безперервно — у відповідь на дії користувача, відповіді сервера й таймери.
Важливо, де це дерево живе. JS-рушій виконується в тому самому головному потоці, що й побудова дерев та розрахунок геометрії, тож довгий синхронний JavaScript блокує рендеринг — сторінка замерзає. Ролі при цьому розділені: (rendering engine) відповідає за структуру й вигляд, JS-рушій — за поведінку, а потік у них спільний.
Поза цим потоком браузер робить чимало: мережеві запити, таймери, читання файлів, декодування зображень виконують окремі системні механізми. Саме на цьому стику й народжується асинхронність — важку роботу браузер делегує назовні й повертає результат, коли потік JS звільниться. Окремий потік можна взяти й самому — це Web Worker, але доступу до DOM він за задумом не має. Про підсистеми детально — глава Архітектура браузера й рендеринг.
Наша практика (не канон). Джерела реєстру цього не формулюють: DOM фізично живе на боці рушія рендерингу, тож кожен виклик на кшталт document.querySelector — перехід межі між підсистемами, і масові операції з деревом у циклі коштують дорожче, ніж здається з коду.
Гонка станів як наслідок моделі
Гонка (race condition) — ситуація, коли результат залежить від порядку завершення асинхронних операцій, а цей порядок не гарантований. Підґрунтя гонок — сама модель виконання: таймери, ввід-вивід і події лише додають завдання в чергу.
Тонкість, яку варто розуміти точно: всередині одного завдання все детерміновано — воно виконується до завершення, і вивід передбачуваний. Гонка виникає між завданнями.
Лікується це в коді відстеженням актуальності запиту: скасувати попередній через AbortController або ігнорувати відповіді, чий запит уже застарів.
let requestId = 0;
async function search(query: string): Promise<void> {
const current = ++requestId;
const results = await fetchSearch(query);
if (current !== requestId) return; // відповідь на застарілий запит
render(results);
}
Наша практика (не канон). Приклад вище — наша ілюстрація, а не теза джерела. Живий пошук: користувач набирає ab, потім abc, летять два запити, і якщо відповідь на ab прийде пізніше, вона перезапише правильні результати. Суть дефекту одним рядком: перемагає «останній, що відповів», хоча має перемагати «останній, що надіслали». Гонки ми вважаємо головною причиною : тест, який не синхронізується зі станом, сам створює гонку.
Наслідок для тестів: синхронізація замість пауз
Тест і застосунок живуть у різному часі: тест виконує команди швидко й послідовно, браузер робить свою роботу через подієвий цикл у власному темпі — і між «тест наказав» і «браузер зробив» завжди є проміжок.
Частину проміжку інструмент закриває сам. Перед кожною дією він виконує набір перевірок придатності (actionability) — що локатор вказує рівно на один елемент і що елемент видимий, стабільний, отримує події й увімкнений, — чекає, поки релевантні пройдуть, і лише тоді діє; не дочекався в межах — падає з TimeoutError. Web-first самі повторюють перевірку, тому ручні паузи навколо них зайві.
Але не знає прикладних станів: чи навісився обробник після гідратації, чи завершився запит, чи оновилися дані в таблиці. Там потрібне явне очікування сигналу — і одна деталь, на якій ловляться навіть досвідчені: проміс очікування відповіді створюють до дії, що тригерить запит, інакше швидка відповідь прослизне у вікно між ними.
// Погано: пауза синхронізується з годинником, а не із застосунком
await page.waitForTimeout(3000);
await expect(page.locator('#status')).toHaveText('Готово');
// Добре: очікування відповіді створене до кліку, асерт чекає стану
const response = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Зберегти' }).click();
await response;
await expect(page.locator('#status')).toHaveText('Готово');
Фіксована пауза програє двічі: замало — тест на повільному CI, забагато — прогін марно розтягується. Дока Selenium каже те саме прямо: код не може знати, скільки чекати насправді, тож замала пауза падає, а завелика в кожному потрібному місці робить тривалість сесії неприйнятною; явне ж очікування вона описує як цикл, що опитує застосунок на конкретну умову, і два механізми синхронізації називає прямо «better». Виняток «а якщо перевіряємо відсутність зміни» ходить по статтях, але джерельної опори він не має: у доці Selenium про виправданість такої паузи не сказано нічого — ні за, ні проти. Практичні рецепти — у главі Практичні сценарії AQA: флак і синхронізація.
Типові помилки
- «
setTimeout(fn, 0)виконає функцію негайно». Насправді це задача: вона чекає кінця синхронного коду й повного спорожнення черги мікрозадач, а у вкладених таймерах ще й отримує мінімум 4 мс. - «Проміс уже розвʼязаний, тож
.thenспрацює одразу». Насправді функція, передана вthen(), ніколи не викликається синхронно — вона йде в чергу мікрозадач. - «
awaitзупиняє браузер». Насправді призупиняється лише ця функція; потік вільний, а продовження повертається мікрозадачею. - «
fetchне кинув помилку, отже запит успішний». Насправді404і500дають успішний проміс — статус перевіряють вручну. - «Елемент є в DOM, отже можна діяти». Насправді його вміст могла оновлювати мікрозадача чи мережевий колбек, який ще не відпрацював.
- «Клік не спрацював — поганий локатор». Насправді потік міг бути зайнятий довгим синхронним кодом, і подія просто не оброблялась.
- «Обробника немає на кнопці — баг». Насправді це може бути делегування: обробник висить на предку, а перевіряти треба результат кліку.
- «Винесемо роботу з DOM у Web Worker». Насправді доступу до DOM не має: у нього виносять обчислення.
- «Порядок логів не такий, як у коді — щось поламано». Насправді це майже завжди пріоритет мікрозадач над задачами.
- «Тест то падає, то ні — інфраструктура». Насправді це майже завжди гонка між темпом тесту й темпом асинхронного застосунку.
Підсумок
- Один потік і
run-to-completion. Поки поточне завдання не закінчилось, не станеться нічого іншого — ні кліку, ні анімації, ні перемальовування. Звідси і «зависла вкладка», і «мертві» кнопки. - Асинхронність — не паралельність. Таймери, мережа й події лише додають завдання в чергу; момент виконання визначає подієвий цикл, тому асинхронний колбек ніколи не перерве синхронний код.
- Черга мікрозадач спорожнюється повністю після кожного завдання. Тому реакція проміса завжди випереджає
setTimeout(fn, 0), а «макрозадача» — жаргон, якого канонічні джерела не вживають. - Затримка таймера — мінімум, а не розклад. Гарантій щодо часу специфікація не дає, а з шостого рівня вкладеності ще й піднімає до 4 мс затримку, задану меншою.
- Гонка — не , а наслідок моделі. У коді її закривають перевіркою актуальності запиту, у тестах — синхронізацією за спостережуваним станом, а не паузою на годинник.
Можливі питання
- «Що таке event loop і навіщо він, якщо потік один?» Перевіряють, чи розуміє кандидат, що асинхронність — це порядок узяття завдань, а не паралельне виконання. Сильна відповідь називає стек, чергу й правило «наступне завдання — коли стек порожній».
- «У якому порядку виведуться
console.log,setTimeout(0)іPromise.then?» Класика на пріоритет черг. Дивляться не на вгаданий порядок, а на пояснення: спершу синхронний код, потім уся черга мікрозадач, і лише потім задача. - «Чому
setTimeout(fn, 0)не спрацьовує миттєво?» Чекають дві незалежні причини — черга задач і те, що затримка є мінімумом, а не гарантією. Згадка про 4-мс clamp відрізняє прочитане від зрозумілого. - «Чим мікрозадача відрізняється від задачі і що куди потрапляє?» Питання на точність: реакції промісів,
queueMicrotaskіMutationObserverпроти таймерів, подій DOM і XHR — плюс повне спорожнення черги. - «Що станеться зі сторінкою під час довгого синхронного циклу?» Перевіряють звʼязок моделі з UX і з тестами: потік зайнятий, події не обробляються, рендер стоїть, а тест бачить неклікабельний елемент або таймаут.
- «Що таке race condition у браузері та як від неї захиститися?» Чекають означення через порядок завершення операцій і два рівні лікування —
AbortControllerчи перевірка актуальності в коді й синхронізація за станом у тесті. - «Чому фіксована пауза — погана відповідь на нестабільний тест?» Дивляться, чи бачить кандидат, що пауза синхронізується з годинником, а не із застосунком: вона маскує гонку й водночас розтягує прогін.
Джерела
- MDN — JavaScript execution model — однопотоковість виконання JS, кадри стека, run-to-completion, неблокуюча модель і легасі-винятки, що її ламають.
- MDN — Using Web Workers — воркер як окремий потік без доступу до DOM.
- Chrome for Developers — Inside look at modern web browser (part 3) — JS-рушій у головному потоці рендерера і блокування рендерингу довгим синхронним кодом.
Подієвий цикл: хто вирішує, коли виконати відкладене
- WHATWG HTML Standard — Event loops — нормативний опис циклу, черг завдань, джерел завдань і нагоди для рендерингу.
- MDN — JavaScript execution model — спрощена логіка циклу, додавання завдань у кінець черги, «таймери лише додають завдання».
Задачі і мікрозадачі: чому порядок саме такий
- WHATWG HTML Standard — Event loops — «The microtask queue is not a task queue», контрольна точка й повне спорожнення черги мікрозадач.
- MDN — JavaScript execution model — склад задач і мікрозадач, пріоритет мікрозадач, розподіл на tasks and microtasks.
Проміси, async/await і мережа в цій моделі
- MDN — Using promises (JavaScript Guide) — ланцюг замість callback hell, колбеки промісів як мікрозадачі, гарантія несинхронного виклику
then. - TC39 — ECMAScript 2027 Language Specification (ECMA-262, чинний draft) — три взаємовиключні стани проміса як нормативна вимога, безрезультатність повторного
resolve/reject. - MDN — Promise —
settledяк «не pending» і комбінатори для узгодження кількох асинхронних операцій. - MDN — async function — асинхронна функція завжди повертає проміс, межа синхронного до першого
await, продовження як колбек.then. - WHATWG HTML Standard — Event loops — обробка неблокуюче отриманого ресурсу як задача; планування промісних операцій у черзі мікрозадач; диспетчеризація події як задача з модальністю «often» і застереженням, що не всі події йдуть чергою задач.
- MDN — Using microtasks in JavaScript with queueMicrotask() — склад черг: проміси й
MutationObserverу мікрозадачах, спрацьований таймер у черзі задач, клік як задача, що виконує колбеки події.
Таймери: чому нуль не означає нуль
- WHATWG HTML Standard — Timers (§8.6) — джерело задач таймерів, відсутність гарантії розкладу, нормалізація відʼємної затримки, 4-мс clamp з умовою вкладеності й дозвіл на додаткову затримку.
- MDN — JavaScript execution model — таймер лише додає задачу в чергу, момент виконання визначає цикл.
AJAX і fetch: звідки береться проміжок
- MDN — Using the Fetch API — fetch як заміна XHR на промісах, успішний проміс на
404,response.okдля 2xx, відкритий перелік причин реджекту. - MDN Glossary — SPA (Single-page application) — обмін даними у фоні й оновлення частини сторінки без перезавантаження.
- MDN — XMLHttpRequest: readyState property —
DONEяк «завершилось успішно або з помилкою». - Playwright — Network — перехоплення запитів обох механізмів через мережевий стек браузера.
- WHATWG DOM Standard — три фази поширення,
eventPhase, точний порядок слухачів,stopPropagation,stopImmediatePropagation,preventDefault, делегування іe.target. - W3C — UI Events — спливання
focusin/focusoutпротиblurі нормативний порядок цих подій. - MDN — Introduction to events — свідомий
preventDefault()в обробнику як причина «клік нічого не зробив».
DOM і головний потік: що ділиться, а що виконується поза ним
- MDN — Introduction to the DOM — дерево DOM як структура в памʼяті, яку скрипти змінюють після завантаження.
- Chrome for Developers — Inside look at modern web browser (part 3) — спільний головний потік рендерера і блокування рендерингу довгим синхронним JavaScript.
- MDN — Rendering engine (Glossary) — межа між рушієм рендерингу і рушієм, що виконує код.
- MDN — Using Web Workers — воркер як окремий потік без доступу до DOM.
Гонка станів як наслідок моделі
- MDN — JavaScript execution model — відсутність гарантій щодо порядку задач і повна обробка кожного завдання перед наступним.
- MDN — Using the Fetch API — скасування запиту через
AbortControllerіsignal.
Наслідок для тестів: синхронізація замість пауз
- Playwright — Auto-waiting (actionability) — набір перевірок придатності, падіння з
TimeoutError, самоповторні асерти й межі автоочікування. - Selenium — Waiting Strategies — подвійна ціна фіксованої паузи, явне очікування як цикл опитування умови, два механізми синхронізації як «better».
- Playwright — class Page — очікування мережевої відповіді, створене до дії, що тригерить запит.
Що означає однопотоковість JavaScript у браузері?
У кожен момент вкладка обробляє рівно один фрагмент коду — двох ваших скриптів одночасно не буває. Механіка проста: виклик функції кладе кадр (frame) на стек викликів (call stack), повернення знімає його, і кожна порція роботи стартує та завершується на порожньому стеку. Асинхронність цього не скасовує: мережу, таймери, читання файлів браузер віддає підсистемам поза цим потоком, а назад приходить лише готовий результат у вигляді відкладеного . Тому «асинхронно» тут читається як «пізніше, коли потік звільниться», а не «паралельно». Практичний наслідок для тестів: поки потік чимось зайнятий, сторінка не обробляє ні кліків, ні анімацій, ні перемальовування — і «елемент не клікається» часто виявляється не проблемою .
Що таке стек викликів і що з нього видно, коли тест упав?
Стек — це поточний ланцюг незавершених викликів: хто кого викликав, згори вниз. У stack trace найвищий рядок показує функцію, у якій стався збій, нижчі — тих, хто до неї привів, тож читати трасу треба саме зверху. Коли в зринає RangeError: Maximum call stack size exceeded, читати це треба однозначно: кадри додаються швидше, ніж знімаються, і майже завжди винна рекурсія без умови виходу. Для AQA це корисний скіл дебагу: за трасою одразу видно, у якій функції стався збій і яким ланцюжком викликів до неї дійшли. Ще одна деталь — межа порції роботи: якщо стек порожній, поточне завдання завершено, і браузер може взяти наступне.
Що таке run-to-completion і як воно проявляється?
Це правило, за яким поточне завдання доопрацьовується до кінця, і лише потім браузер бере наступне; примусово перервати виконання JS ніхто не може. Наслідок номер один: асинхронний колбек не вклинюється всередину синхронної функції — навіть якщо його «час» уже настав, він просто чекає своєї черги. Наслідок номер два: усе, що є всередині одного завдання, детерміноване, тому шукати гонки треба між завданнями, а не всередині них. Наслідок номер три — той самий «зависання вкладки»: важкий цикл, синхронний розбір великого JSON чи масова правка дерева тримають потік, і на цей час сторінка мертва. Окремо варто памʼятати про легасі-API, які блокують потік навмисно, — alert() і синхронний XMLHttpRequest.
Навіщо потрібен подієвий цикл (event loop), якщо потік і так один?
— це механізм, який вирішує, коли брати відкладену роботу: узгоджує події, дії користувача, скрипти, мережу й рендеринг у межах одного потоку. Логіка циклічна: виконати синхронний код, дочекатися порожнього стека, спорожнити мікрозадачі, узяти наступне завдання з черги, повторити. Ключове для розуміння: таймери, мережа й події самі нічого не виконують — вони лише додають завдання, а момент виконання призначає цикл. Черг при цьому кілька, і кожне джерело (події, парсер, таймери) прив'язане до своєї, щоб порядок усередині джерела не ламався; формально цикл вибирає не буквально перше завдання в списку, а перше, яке вже можна виконувати. Потоком операційної системи цикл не є — це логічна конструкція, і кілька циклів можуть співіснувати в одному потоці.
У якому порядку виведеться цей код і чому?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
Порядок буде A, D, C, B. Спершу відпрацьовує весь синхронний код — це A і D; setTimeout і .then на цьому етапі лише реєструють відкладену роботу. Коли стек порожніє, цикл іде на контрольну точку мікрозадач і спорожнює їхню чергу — звідси C. І тільки після цього береться задача таймера, тобто B, хоч у коді вона записана раніше за . На співбесіді оцінюють не вгаданий порядок, а саме це пояснення: синхронний код — уся черга мікрозадач — одна задача.
Чим задача відрізняється від мікрозадачі і що куди потрапляє?
Це дві різні сутності, і черга мікрозадач не є однією з черг задач — так прямо сказано в HTML Standard. До задач належать колбеки setTimeout і setInterval, події DOM та події XMLHttpRequest. До мікрозадач — реакції промісів (then, catch, finally), queueMicrotask і колбеки MutationObserver. Різниця у правилі обробки: задачі беруться по одній за прохід циклу, а мікрозадачі спорожнюються всі одразу після поточного завдання, разом із тими, що додалися просто під час спорожнення. Звідси і вищий пріоритет мікрозадач, і неприємний крайній випадок: код, який безперервно плодить нові мікрозадачі, замикає цикл на собі — ані наступна задача, ані перемальовування вже не настануть.
Чи коректно вживати термін «макрозадача»?
У побутових розмовах і статтях так кажуть часто, але канон цього слова не знає: ані MDN, ані HTML Living Standard терміна macrotask не містять — там є рівно два поняття, tasks і microtasks. Тобто «макрозадача» — жаргонний синонім до «задачі», який виник для симетрії з «мікрозадачею». Практична порада: розпізнавати його на слух і розуміти, що співрозмовник має на увазі звичайну задачу, а самому в поясненні вживати канонічну пару. На співбесіді це дрібниця, але вона добре показує, читав кандидат специфікацію чи переказує чужий переказ.
Чому setTimeout(fn, 0) не виконує функцію відразу?
Причин дві, і вони незалежні. Перша — черга: колбек стає звичайною задачею з джерела задач таймерів, тож він у будь-якому разі дочекається, поки догорить синхронний код і спорожніє вся черга мікрозадач. Друга — сама природа : специфікація не обіцяє, що таймер спрацює точно за розкладом, бо на це впливають завантаження процесора й інші завдання; затримка — це мінімум, а не домовленість про час. Відʼємне значення нормалізується в нуль, тож -100 нічого не пришвидшує. Тому setTimeout(fn, 0) варто читати як «перекласти роботу на дальший прохід циклу», а не як «виконати зараз» і тим паче не як спосіб щось виміряти.
Що таке 4-мілісекундний clamp у таймерах і коли він вмикається?
Це жорстка підлога затримки для вкладених таймерів: коли рівень вкладеності перевищує 5, а замовлена затримка не дотягує до 4 мілісекунд, браузер піднімає її до чотирьох. Порогова умова — саме «більший за 5», тобто обмеження вмикається з шостого рівня. Далі дві деталі, які відрізняють завчене від зрозумілого. Перша: рівні рахує сам алгоритм таймера для своїх вкладених викликів, тому setInterval, який повторюється циклічно, набирає їх без жодного ручного вкладання. Друга: чотири мілісекунди — це підлога, а не гарантований крок; браузеру окремо дозволено накинути ще затримки на визначений реалізацією час, наприклад щоб зекономити енергію на пристрої з режимом низького споживання. Для тестів висновок один: будувати очікування на «таймер спрацює приблизно тоді» не можна.
Чи може колбек .then() виконатися синхронно, якщо проміс уже розвʼязаний?
Ні, і це гарантія мови, а не випадковість реалізації. Функція, передана в then(), у будь-якому разі стає мікрозадачею й дочекається порожнього стека — навіть якщо проміс був успішним ще до підписки. Зроблено це заради передбачуваності: інакше та сама функція поводилася б по-різному залежно від того, чи встиг проміс завершитися. Стани проміса взаємовиключні (pending, fulfilled, rejected), і завершений (settled) проміс уже не переграєш — друга спроба викликати resolve чи reject мовчки нічого не змінить. У тестах це пояснює класику: після дії, що змінює стан, значення в DOM може оновитися не в тому ж такті, а мікрозадачею — тому має чекати стан, а не читати його одразу.
Що насправді робить await — блокує сторінку?
Ні, заморожується лише конкретна асинхронна функція, а потік лишається вільним і далі обробляє події та рендер. Розкладка така: назовні така функція в підсумку віддає проміс; усе до першого await включно відпрацьовує синхронно, а хвіст після кожного await оформлює як колбек .then — тобто ставить у чергу мікрозадач. Тому await не «зупиняє час», а віддає керування циклу до моменту, коли очікуваний проміс завершиться. Наслідок, який легко проґавити: між рядком з await і наступним рядком стан сторінки міг змінитися. І окремо про тести — await у коді автотесту чекає завершення промісу інструмента, тобто дає синхронізацію з браузером, а не паузу.
Мережева відповідь — це задача чи мікрозадача?
І те, й інше, тільки на різних етапах. Завершення запиту браузер оформлює як задачу, і саме вона переводить проміс у завершений стан. А вже реакції .then / .catch, навішані на цей проміс, виконуються мікрозадачами — після тієї задачі, у межах найближчого спорожнення черги. Старий XMLHttpRequest тут поводиться інакше: його onload лишається звичайною задачею, без мікрозадачного етапу. Модальність варто передати як у стандарті: він каже, що диспетчеризацію події «often» робить окрема задача, і одразу застерігає, що не всі події йдуть чергою задач — тож коректно казати «обробник події лишається задачею, а не мікрозадачею», а не «завжди створює окрему задачу». Практичний висновок для дебагу: між «відповідь прийшла» і «дані на екрані» є щонайменше один такт циклу, і саме в цей проміжок падають тести, які читають DOM одразу після мережевої події.
Чому fetch() не кидає помилку на 404 і 500, і що з readyState === 4 у XHR?
Це дві пастки з тієї самої родини — «немає винятку, отже все добре». fetch() реджектиться не на статусі відповіді, а на деяких помилках — документація називає два приклади, мережеву помилку й хибну схему URL, і перелік навмисно лишає відкритим; а 404 чи 500 дають цілком успішний проміс з обʼєктом Response, тож статус треба перевіряти руками. Найкоротший спосіб — response.ok, який істинний тільки для діапазону 2xx. У XMLHttpRequest схожа історія зі станом 4 DONE: він означає, що операція завершилась, а завершилась вона успішно чи з помилкою — окреме питання. Для автотестів це прямо перекладається в правило: перевіряй статус явно, інакше зелений тест мовчки проковтне зламаний бекенд.
Які фази проходить подія DOM і в якому порядку спрацьовують обробники?
Фаз три: захоплення (capturing) — рух від кореня до цілі, фаза цілі, і спливання (bubbling) — назад угору; поточну фазу можна прочитати з властивості eventPhase. Порядок нормативний: спочатку зверху вниз відпрацьовують слухачі з прапорцем capture: true, а далі — якщо подія взагалі спливає — усі інші, у зворотному напрямку. За замовчуванням слухач підписується саме на спливання, тому в звичайному коді фаза захоплення просто не помітна. Зі спливання прямо випливає делегування (event delegation): достатньо одного слухача на спільному предку, щоб обслужити всіх нащадків, зокрема й тих, кого додадуть пізніше, а e.target покаже найглибший елемент під курсором у момент кліку. Для тестувальника звідси корисне правило: відсутність обробника на самій кнопці ще не баг — перевіряти треба результат кліку.
Чим різняться stopPropagation(), stopImmediatePropagation() і preventDefault()?
Це три різні інструменти, і плутанина між ними дає дуже схожі симптоми. stopPropagation() не пускає подію далі по дереву, але стандартну дію браузера не чіпає — посилання все одно перейде, форма все одно відправиться. stopImmediatePropagation() робить те саме плюс відрубує решту слухачів на цьому ж елементі — тих, що мали спрацювати після поточного. preventDefault() навпаки: поширення не спиняє, а скасовує стандартну дію — і лише для подій, які взагалі можна скасувати (cancelable: true); чи спрацювало, видно з defaultPrevented. Типовий баг-репорт «клік нічого не робить» дуже часто закінчується саме тут: обробник свідомо викликав preventDefault(), і це не помилка застосунку, а його логіка.
Чому делегування для набору полів будують на focusout, а не на blur?
Бо спливання є не в кожної події: у blur його немає, у focusout є, а делегування тримається саме на спливанні. Тому слухач, повішений на форми, побачить focusout з будь-якого поля всередині, тоді як blur до нього просто не дійде — його довелося б вішати на кожне поле окремо. Порядок цих подій теж нормативний: focus спрацьовує раніше за focusin, а blur — раніше за focusout. Для AQA це практичний детектив: якщо валідація поля «іноді не запускається», варто перевірити, на яку саме подію вона підписана і чи справді вона спливає. Той самий підхід рятує при роботі з динамічними списками, де рядки додаються після завантаження.
Сторінка застрягла на важкому синхронному циклі — що з нею відбувається і чим допоможе Web Worker?
Сторінка стане непрацездатною до кінця цього циклу: події не обробляються, анімації стоять, перемальовування не відбувається — потік зайнятий, і виконання ніхто не витіснить. Причина в тому, що JS-рушій ділить головний потік із тим, хто будує дерева й рахує геометрію: за вигляд відповідає , за поведінку — JS-рушій, а потік у них один на двох. Виходів два: порізати роботу на дрібніші порції, щоб цикл встигав дихати між ними, або віддати розрахунки у Web Worker — це справді окремий потік. Але важлива умова: доступу до DOM у немає за задумом, тому «винесемо роботу з дерева у воркер» не працює — виносять саме розрахунки. У тестах симптом такий: елемент видно на , а клік або асерт падає з , бо застосунок фізично не встиг відповісти.
Що таке гонка станів (race condition) у браузері і як від неї захищаються в коді?
Гонка — це коли результат залежить від того, яка з асинхронних операцій завершиться першою, а порядок їхнього завершення ніхто не гарантує. Підстава для неї закладена в самій моделі: таймери, ввід-вивід і події лише кладуть завдання в чергу, і черговість відповідей не зобовʼязана збігатися з черговістю запитів. Хрестоматійний приклад — живий пошук: у поле друкують ab, за мить abc, вилітають два запити, і якщо відповідь на коротший рядок приходить із запізненням, вона затирає актуальні результати. Суть дефекту в одному рядку: виграє той, хто відповів останнім, хоча має вигравати той, кого надіслали останнім. Лікують це двома прийомами — скасувати застарілий запит через AbortController або пронумерувати запити й ігнорувати відповідь, чий номер уже неактуальний. Важливий нюанс для розуміння: усередині одного завдання все детерміновано, гонка живе саме між завданнями.
Чому фіксовану паузу вважають поганим лікуванням нестабільного тесту?
Бо вона узгоджена з годинником, а не зі станом застосунку: тест не знає, скільки насправді триватиме робота браузера, тож будь-яке число буде або замалим, або завеликим. Замале падає на повільному CI, завелике множиться по всій й перетворює прогін на вічність — Selenium у своїй документації описує цю дилему прямо й радить замість пауз чекати конкретної умови. Гірше те, що пауза маскує причину: гонка нікуди не дівається, вона просто рідше спрацьовує, і дефект перетворюється на плаваючий. Правильна альтернатива — очікування спостережуваного стану: web-first асерти, які самі повторюють перевірку, або явне очікування конкретного сигналу (відповіді, зміни тексту, зникнення спінера). Єдиний виправданий випадок для паузи — коли треба переконатися, що зміни не відбувається — наприклад, що після дії лічильник не поїхав.
Що інструмент автоматизації робить за вас сам, а що доводиться синхронізувати руками?
Playwright перед кожною дією проганяє перевірки придатності (actionability): що під локатор підпадає рівно один елемент, що він не прихований, не рухається, не перекритий чужим шаром і не задизейблений — і чекає цього в межах таймауту, інакше падає з TimeoutError. Асерти теж самоповторні, тож обгортати їх ручними паузами немає сенсу. Але нічого не знає про прикладні стани: чи навісився обробник після гідратації, чи доїхав фоновий запит, чи вже перемалювалися рядки таблиці — це доводиться чекати явно. І тут є класична пастка з мережею: підписку на відповідь оформлюють до дії, що запускає запит, інакше швидка відповідь проскочить у зазор між кліком і підпискою, а тест провисить до таймауту.
// підписка оформлена ДО кліку — відповіді нема куди прослизнути
const created = page.waitForResponse((r) => r.url().endsWith('/api/invoices'));
await page.getByRole('button', { name: 'Створити рахунок' }).click();
await created;
await expect(page.getByTestId('invoice-status')).toHaveText('Проведено');Три кейси, у яких модель виконання перестає бути теорією: розбір класичної задачі на порядок виводу (та сама, яку дають на співбесідах), таблиця діагнозів для «клік не спрацював», і переписування флакучого тесту з паузи на синхронізацію за станом.
Кейс 1. Задача на порядок виводу: розбираємо по тактах
Дають фрагмент і питають, що надрукується. Вгадувати не треба — треба вміти пройти його як цикл.
console.log('1 sync');
setTimeout(() => console.log('2 timeout'), 0);
Promise.resolve().then(() => {
console.log('3 microtask');
return Promise.resolve();
}).then(() => console.log('4 microtask chain'));
(async () => {
console.log('5 sync у async');
await null;
console.log('6 after await');
})();
console.log('7 sync');
Вивід:
1 sync
5 sync у async
7 sync
3 microtask
6 after await
4 microtask chain
2 timeout
Як до цього дійти:
- Спершу все синхронне. Рядки
1,5і7друкуються ще до того, як цикл узагалі щось вирішує. Асинхронна функція не є винятком: її тіло виконується синхронно до першогоawait, тому5стоїть між1і7, а не десь у кінці. setTimeoutі.thenна цьому етапі нічого не виконують. Вони лише реєструють роботу: перший — задачу, другі — мікрозадачі. Порядок рядків у коді на момент виконання вже не важить, важить тип черги.- Стек порожній — цикл спорожнює мікрозадачі. Друкується
3, потім6(продовження післяawait— це така сама мікрозадача, якthen). А4виходить після6тому, що другийthenстає в чергу лише коли відпрацював перший, — продовження післяawaitтам уже стояло; повернений із першогоthenдодає ще й окремі такти на «розгортання». - Задача — остання.
2друкується після того, як черга мікрозадач спорожніла повністю. Саме томуsetTimeout(fn, 0)завжди програє промісу, хоч записаний вище. - Що насправді перевіряють. Не вивчений напамʼять список, а вміння сказати три речення: спершу синхронна частина, далі вся черга мікрозадач, і тільки потім одна задача. Якщо кандидат помиляється в парі
4/6, але правильно називає правило — це сильна відповідь.
Кейс 2. «Клік не спрацював»: таблиця діагнозів
Найдорожчий час у дебагу автотестів витрачається на те, щоб перебрати там, де локатор ні до чого. Ось як розкладаються симптоми.
| Симптом у тесті | Найімовірніша причина | Як перевірити |
|---|---|---|
| Клік «пройшов», але нічого не змінилось | обробник викликав preventDefault() — це логіка застосунку, не баг | у DevTools вкладка Elements → Event Listeners на елементі; у консолі перевірити defaultPrevented події |
Дія падає з TimeoutError, елемент видно на скріншоті | потік зайнятий довгим синхронним кодом, сторінка фізично не відповідає | панель Performance: довге суцільне завдання в Main; або трейс Playwright — дія висить на перевірках придатності |
| Обробника на кнопці немає, а в застосунку клік працює | делегування: слухач висить на предку | шукати слухача на батьківських вузлах; перевіряти не наявність обробника, а результат кліку |
| Валідація поля спрацьовує не завжди | підписка на blur, який не спливає, замість focusout | Event Listeners на контейнері форми; перевірити, чи є слухач на кожному полі |
| Текст оновлюється «через раз» одразу після мережевої відповіді | між задачею відповіді й записом у DOM є такт мікрозадач | чекати спостережуваний стан, а не факт відповіді; звірити з вкладкою Network момент приходу тіла |
Тест зелений, хоча бекенд віддав 500 | код застосунку не перевірив статус — fetch на HTTP-помилці не реджектиться | Network: статус відповіді проти того, що показує UI; додати асерт на статус в API-тесті |
Що з цього випливає для процесу:
- Спершу «хто зайняв потік», потім «чи правильний локатор». Якщо дія падає, а елемент видно, ставка на зайнятий потік або незавершений прикладний стан вища, ніж на помилку в селекторі.
- бреше про час. Він показує пікселі, а не готовність: сторінка може бути намальована, поки обробник ще не навісився після гідратації.
- видно приладом. У панелі Performance вона виглядає як суцільний блок у головному потоці; це той самий інтервал, у який браузер не обробляє події й не перемальовує.
Кейс 3. Флакучий тест: від паузи до синхронізації
Типовий тест на форму замовлення, який «іноді падає на CI». Спершу як його зазвичай пишуть.
import { test, expect } from '@playwright/test';
test('замовлення зберігається', async ({ page }) => {
await page.goto('https://app.example.com/orders/new');
await page.getByRole('button', { name: 'Оформити замовлення' }).click();
await page.waitForTimeout(3000); // пауза за годинником
const status = await page.getByTestId('order-status').textContent();
expect(status).toBe('Готово'); // одноразова перевірка
});
Тут дві помилки, і обидві родом із моделі виконання. Пауза припускає, що застосунок укладеться в три секунди — на повільному CI не вкладеться. А textContent() знімає стан рівно один раз, тобто читає DOM у випадковий такт циклу; якщо запис у дерево ще чекає своєї мікрозадачі, тест побачить попереднє значення.
Робочий варіант:
import { test, expect } from '@playwright/test';
test('замовлення зберігається', async ({ page }) => {
await page.goto('https://app.example.com/orders/new');
// підписка оформлена ДО дії: інакше швидка відповідь проскочить у зазор
const saved = page.waitForResponse(
(r) => r.url().includes('/api/orders') && r.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Оформити замовлення' }).click();
const response = await saved;
expect(response.status()).toBe(201); // мережевий контракт
// web-first асерт сам повторює перевірку до таймауту
await expect(page.getByTestId('order-status')).toHaveText('Готово');
});
Що дивитися і чому:
- Порядок «підписка → дія» не косметичний. Якщо створити очікування після кліку, а сервер відповість за 20 мс, подія настане раніше за підписку — і тест чекатиме відповіді, якої вже не буде, до самого . Це один із найчастіших «зависань» у зелених на локалці .
- на статус потрібен окремо.
fetchу застосунку не кине помилку на500, тому UI може намалювати щось нейтральне; без перевірки статусу тест мовчки прийме зламаний бекенд. toHaveTextзамістьtextContent(). Перший опитує стан до збігу або таймауту, другий фіксує одну мить. Ручні паузи навколо самоповторних асертів зайві — вони лише додають секунди до прогону.- Що інструмент чекає сам, а що ні. Перед кліком він перевіряє, що під локатор підпадає один елемент, що той не прихований, не рухається, не перекритий чужим шаром і не задизейблений. Але про те, чи завершився фоновий запит і чи вже навісився обробник після гідратації, він не знає нічого — це ваша частина роботи.
- Коли пауза все ж доречна. Коли перевіряєте, що зміни не відбувається: наприклад, що після кліку по задизейбленій кнопці лічильник не зрушив. Тут чекати нема чого — треба саме витримати інтервал і переконатися, що стан той самий.
Окремий підклас того самого дефекту — гонка в самому застосунку, яку тест лише проявляє. Живий пошук шле запит на кожен ввід, і якщо відповідь на короткий запит приходить останньою, вона перезаписує правильні результати. У коді це закривають скасуванням або перевіркою актуальності:
let lastRequestId = 0;
async function runSearch(query: string): Promise<void> {
const id = ++lastRequestId;
const items = await fetchSearch(query);
if (id !== lastRequestId) return; // це відповідь на вже неактуальний запит
renderResults(items);
}
У баг-репорті на таку поведінку варто писати не «пошук показує не те», а точний механізм: перемагає той, хто відповів останнім, замість того, кого надіслали останнім. Це різниця між тікетом, який повертають з питаннями, і тікетом, який беруть у роботу.
Один потік і стек викликів
- Розумію, що вкладка виконує рівно один фрагмент коду за раз, а «асинхронно» означає «пізніше, коли звільниться потік», а не «паралельно».
- Можу пояснити роль стека викликів (call stack) і те, як читати stack trace: згори — місце збою, нижче — ті, хто туди привів.
- Можу пояснити
run-to-completionі його наслідок: асинхронний ніколи не вклинюється в синхронний код. - Розумію, чому довгий синхронний код вішає сторінку повністю — ні подій, ні анімацій, ні перемальовування — і що
alert()та синхронний XHR блокують потік навмисно. - Знаю, що Web Worker дає окремий потік під обчислення, але доступу до DOM у нього немає за задумом.
Подієвий цикл, задачі й мікрозадачі
- Розумію, що таймер, мережа чи подія лише ставлять завдання в чергу, а момент його виконання призначає цикл.
- Знаю розподіл (мікрозадачі — реакції ,
queueMicrotask,MutationObserver; задачі — таймери, події DOM і XHR) і те, що черга мікрозадач стоїть окремо та спорожнюється цілком. - Можу пояснити порядок виводу в задачі «
console.log+setTimeout(0)+Promise.then» через пріоритет черг, а не з памʼяті.
Таймери
- Можу назвати дві незалежні причини, чому
setTimeout(fn, 0)не миттєвий: черга задач плюс те, що задає лише нижню межу, а не момент спрацювання. - Знаю правило clamp: з шостого рівня вкладеності затримка, менша за
4мс, піднімається до чотирьох, і це підлога, а не гарантований крок.
Проміси, async/await і мережа
- Можу пояснити, чому колбек
then()не запускається синхронно, навіть коли проміс завершився ще до підписки, і чомуawaitпризупиняє одну функцію, а не сторінку. - Памʼятаю розкладку мережі: завершення запиту приходить задачею, а реакції
.then/.catchвиконуються мікрозадачами;XHR.onloadлишається задачею — саме задачею, а не мікрозадачею, бо «завжди окрема задача» стандарт не обіцяє. - Знаю, що
fetch()не реджектиться на404чи500— статус перевіряю черезresponse.okабо явно, аreadyState === 4означає лише «завершилось», не «успішно».
Події DOM
- Можу назвати три фази поширення події й порядок слухачів: спершу
capture: trueзгори вниз, потім решта — на спливанні знизу вгору. - Не плутаю
stopPropagation(),stopImmediatePropagation()іpreventDefault()та памʼятаю, що останній не спиняє поширення. - Знаю, що спливання є в
focusoutі немає вblur, тому делегування для набору полів вішають саме наfocusout.
Гонки й синхронізація тестів
- Можу дати означення гонки через порядок завершення операцій, знаю, що всередині одного завдання все детерміновано, і назву два ліки в коді:
AbortControllerабо перевірку актуальності відповіді. - Можу пояснити, що інструмент чекає сам (перевірки придатності, самоповторні ), а що доводиться чекати явно (прикладні стани, конкретні відповіді).
- Розумію, чому фіксована пауза програє завжди: замала падає на повільному CI, завелика розтягує прогін, і обидві маскують гонку замість лікувати.
- Памʼятаю, що підписку на мережеву відповідь оформлюють до дії, яка запускає запит, інакше швидка відповідь проскочить у зазор.
Квіз
Перед стартом
- Питань: 15
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Що означає «асинхронно» в браузерному JavaScript?