Коротка відповідь. JavaScript виконує код в одному потоці. Event loop – механізм, який після завершення синхронного коду по черзі бере відкладені задачі: спочатку ВСІ мікрозадачі (колбеки промісів, queueMicrotask), потім одну макрозадачу (setTimeout, події, I/O) – і так по колу. Саме тому Promise.then завжди виконається раніше за setTimeout(…, 0). Нижче – механіка по кроках і три задачі, які реально ставлять на співбесідах.
Три складові, які треба назвати
- Call stack – стек викликів. Синхронний код виконується тут і зараз; поки стек не порожній, нічого «відкладене» не запуститься.
- Черга макрозадач (task queue) – колбеки
setTimeout/setInterval, обробники подій, I/O. За одну ітерацію циклу виконується одна макрозадача. - Черга мікрозадач (microtask queue) – колбеки
.then/.catch/.finally, продовження післяawait,queueMicrotask. Після кожної макрозадачі (і після початкового синхронного коду) виконуються всі накопичені мікрозадачі, включно з тими, що додалися під час виконання.
Алгоритм одним реченням для співбесіди: «виконати синхронний код → спорожнити всі мікрозадачі → взяти одну макрозадачу → знову всі мікрозадачі → повторювати».
Чому Promise.then раніше за setTimeout
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// start, end, promise, timeoutРозбір по кроках:
'start'– синхронно, виконується одразу.setTimeoutреєструє колбек у черзі макрозадач (навіть із затримкою 0)..thenреєструє колбек у черзі мікрозадач.'end'– синхронний код закінчився, стек порожній.- Event loop спершу спорожнює мікрозадачі →
'promise'. - Лише потім бере макрозадачу →
'timeout'.
Типова неправильна відповідь: «setTimeout з нулем виконається одразу». Ні: нуль означає «не раніше ніж», а не «зараз»; колбек усе одно чекає своєї черги після мікрозадач.
Задача 2: async/await – це теж мікрозадачі
async function foo() {
console.log('foo start');
await null;
console.log('foo after await');
}
console.log('script start');
foo();
console.log('script end');
// script start, foo start, script end, foo after awaitКлючовий момент: тіло async-функції до першого await виконується синхронно. Сам await розрізає функцію: усе, що після нього, стає мікрозадачею. Тому 'foo after await' виводиться після 'script end', але без участі жодного таймера.
Типова неправильна відповідь: «async-функція вся виконується асинхронно». Ні – асинхронним стає лише продовження після await.
Задача 3: мікрозадачі попереду навіть «старших» таймерів
setTimeout(() => console.log('timeout 1'), 0);
Promise.resolve()
.then(() => console.log('then 1'))
.then(() => console.log('then 2'));
setTimeout(() => console.log('timeout 2'), 0);
// then 1, then 2, timeout 1, timeout 2Обидва таймери зареєстровані раніше, ніж виконався перший .then, але це не має значення: перед першою макрозадачею event loop виконує всі мікрозадачі. Другий .then додається в чергу мікрозадач під час виконання першого – і теж встигає до таймерів, бо черга мікрозадач спорожнюється до кінця.
Небезпечний наслідок для практики: нескінченний ланцюжок мікрозадач (наприклад, рекурсивний queueMicrotask) повністю заблокує таймери й рендеринг – черга макрозадач просто ніколи не отримає хід.
Як це виглядає в браузері і навіщо це знати
Подія кліку, відповідь fetch, таймер – усе проходить через ці черги. Практичні наслідки, про які варто сказати на співбесіді:
- Довга синхронна операція «підвішує» інтерфейс: поки стек зайнятий, жоден обробник події не виконається.
setTimeout(fn, 0)– легальний спосіб відкласти роботу «після поточного кадру подій», але не гарантія часу.- Порядок виводу визначається типом черги, а не порядком рядків у коді.
Довідкове першоджерело з термінологією – розділ про concurrency model на MDN.
Задача 4 (комбінована): await + setTimeout разом
Фінальний рівень – коли в одному фрагменті і async-функція, і таймер, і ланцюжок then:
async function main() {
console.log('1');
setTimeout(() => console.log('2'), 0);
await Promise.resolve();
console.log('3');
Promise.resolve().then(() => console.log('4'));
console.log('5');
}
main();
console.log('6');
// 1, 6, 3, 5, 4, 2Розбір: '1' синхронно; таймер у макрочергу; await розрізає функцію – решта стає мікрозадачею; тому далі синхронний '6'. Потім мікрозадача: '3', реєстрація then ('4' – у кінець мікрочерги), '5'. Мікрочерга ще не порожня → '4'. І лише тепер макрозадача → '2'. Якщо ти можеш пояснити цей приклад без запинки – тема закрита.
А де тут рендеринг
У браузері між макрозадачами рушій може виконати рендеринг (перерахунок стилів, layout, paint). Практичний висновок для співбесіди з frontend-ухилом: важка синхронна робота або нескінченні мікрозадачі відкладають не лише таймери, а й відмалювання – сторінка «замерзає». Тому великі обчислення ріжуть на шматки (через setTimeout/queueMicrotask свідомо або Web Workers), а анімації прив'язують до requestAnimationFrame, який викликається перед кадром рендерингу.
Типові неправильні відповіді кандидатів
- «JavaScript багатопотоковий, бо є асинхронність». Ні: потік виконання один; асинхронність – це черги, а не паралельність.
- «Мікро- і макрозадачі – одна черга з пріоритетами». Ні: це дві різні черги з різними правилами спорожнення (усі проти однієї).
- «await зупиняє весь JavaScript». Ні: він призупиняє лише поточну async-функцію; решта коду виконується далі.
- «Порядок setTimeout(0) і Promise залежить від браузера». Ні: порядок визначений специфікацією і стабільний.
Швидка самоперевірка перед співбесідою
Відповідай уголос, без підглядання:
- Скільки макрозадач виконується за одну ітерацію циклу? (Одна.)
- Скільки мікрозадач? (Усі в черзі, включно з доданими під час виконання.)
- Куди потрапляє код після await? (У чергу мікрозадач.)
- Чи може setTimeout(fn, 0) виконатися раніше за .then, зареєстрований пізніше? (Ні – мікрозадачі завжди попереду наступної макрозадачі.)
Якщо на будь-якому пункті завагався – повернись до відповідної задачі вище й прожени її в консолі.
Як закріпити
Візьми будь-який приклад із цієї статті, зміни його (додай другий await, вклади setTimeout у then) і спочатку запиши очікуваний вивід на папері, потім запусти. Розрив між прогнозом і реальністю – твій список для повторення. Ця сама навичка «прогнозувати виконання» перевіряється й у задачах із добірки live coding, а системну підготовку зручно будувати за планом зі статті про підготовку за 30 днів. Асинхронні патерни поверх цієї механіки – в окремому розборі async/await і Promise.