- Описывать стек вызовов, очередь задач и очередь микрозадач
- Предсказывать порядок вывода в коде, где смешаны синхронный код,
setTimeout, промисы иawait - Выбирать между последовательным и параллельным асинхронным кодом:
Promise.all,allSettled,race,any
Классический вопрос на собеседовании: «В каком порядке этот код что-то выведет?» — и перед тобой смесь console.log, setTimeout(..., 0) и Promise.then. Даже опытные разработчики часто ошибаются. JavaScript работает в одном потоке, но обрабатывает тысячи кликов, запросов и таймеров. Секрет — в цикле событий (event loop). Как только поймёшь три структуры — стек вызовов, очередь задач и очередь микрозадач, — такие головоломки превратятся в простую арифметику.
Стек вызовов
Стек вызовов — это список функций, которые выполняются прямо сейчас. Вызов функции кладёт на вершину стека новый «кадр», возврат из неё — снимает его. Стек у JavaScript один, поэтому в каждый момент выполняется только одно дело. Пока стек занят, больше ничего произойти не может: клики не обрабатываются, таймеры не срабатывают, страница не перерисовывается.
function third() {
console.log('3. third is on top of the stack');
}
function second() {
console.log('2. second calls third');
third();
console.log('4. back in second');
}
function first() {
console.log('1. first calls second');
second();
console.log('5. back in first');
}
first();▸ Ожидаемый результат
1. first calls second 2. second calls third 3. third is on top of the stack 4. back in second 5. back in first
first → second → third. Каждая функция продолжает работу только после того, как завершился вызов внутри неё. Бесконечная рекурсия переполняет стек, и движок выбрасывает ошибку.Задачи и микрозадачи
Когда асинхронная работа (таймер, ответ сети, клик) завершается, её колбэк не выполняется сразу — он ждёт в очереди. Основных очередей две. В очередь задач (macrotask) попадают setTimeout, setInterval, события пользователя и сообщения. В очередь микрозадач — колбэки промисов (then, catch, finally), код после await и queueMicrotask. Цикл событий обрабатывает их так:
- 1Выполнить текущий код до конца
Скрипт (или одна задача) работает, пока стек не опустеет.
- 2Выполнить ВСЕ микрозадачи
Включая новые микрозадачи, добавленные по ходу, — пока очередь полностью не опустеет.
- 3При необходимости отрисовать страницу
Браузер выводит изменения на экран.
- 4Взять ОДНУ задачу из очереди
Например, один колбэк таймера, — и снова к шагу 1.
console.log('A: script start');
setTimeout(() => console.log('B: timeout'), 0);
Promise.resolve()
.then(() => console.log('C: promise 1'))
.then(() => console.log('D: promise 2'));
queueMicrotask(() => console.log('E: microtask'));
console.log('F: script end');▸ Ожидаемый результат
A: script start F: script end C: promise 1 E: microtask D: promise 2 B: timeout
async/await в очереди
await приостанавливает только свою async-функцию; код после await продолжается как микрозадача. Внимание: код до первого await выполняется синхронно — сразу при вызове функции.
async function load() {
console.log('2. load starts: synchronous part');
await null;
console.log('4. after await: microtask');
}
console.log('1. script start');
setTimeout(() => console.log('6. timeout: task'), 0);
load();
Promise.resolve().then(() => console.log('5. then: microtask'));
console.log('3. script end');▸ Ожидаемый результат
1. script start 2. load starts: synchronous part 3. script end 4. after await: microtask 5. then: microtask 6. timeout: task
Очередь микрозадач опустошается полностью перед следующей задачей. Поэтому промис, созданный внутри таймера, выполняется раньше следующего таймера. По той же причине микрозадача, которая бесконечно ставит себя в очередь, заморозит страницу так же, как бесконечный цикл while.
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('-> microtask inside timeout 1'));
}, 0);
setTimeout(() => console.log('timeout 2'), 0);
Promise.resolve().then(() => {
console.log('microtask 1');
setTimeout(() => console.log('timeout 3, added by microtask 1'), 0);
});▸ Ожидаемый результат
microtask 1 timeout 1 -> microtask inside timeout 1 timeout 2 timeout 3, added by microtask 1
Последовательно и параллельно
JavaScript однопоточный, но ожидания могут перекрываться: сетевые запросы и таймеры отслеживает в фоне браузер (или Node.js). Это называется конкурентностью: одновременно «в работе» много дел, но в каждый момент выполняется только один кусок JavaScript. Частая ошибка: await внутри цикла отправляет запросы один за другим. Если задачи независимы, запусти их все вместе и жди через Promise.all.
const task = (label, ms) =>
new Promise((resolve) => {
setTimeout(() => {
console.log(`${label} done`);
resolve(label);
}, ms);
});
console.log('Sequential:');
await task('A (200 ms)', 200);
await task('B (100 ms)', 100);
console.log('Parallel:');
const results = await Promise.all([task('A (200 ms)', 200), task('B (100 ms)', 100)]);
console.log(results);▸ Ожидаемый результат
Sequential: A (200 ms) done B (100 ms) done Parallel: B (100 ms) done A (200 ms) done [ 'A (200 ms)', 'B (100 ms)' ]
Promise.all возвращает результаты в порядке массива. await на верхнем уровне работает здесь, как в модулях.| Метод | Когда завершается | Результат |
|---|---|---|
Promise.all | когда все успешны или при первой ошибке | массив значений или первая ошибка |
Promise.allSettled | когда все завершились; никогда не отклоняется | массив { status, value } / { status, reason } |
Promise.race | когда завершится первый (успех или ошибка) | самый быстрый исход |
Promise.any | при первом успехе; если все неудачны — ошибка | первое успешное значение или AggregateError |
const ok = (value, ms) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (reason, ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
const settled = await Promise.allSettled([ok('menu', 100), fail('no stock', 50)]);
console.log(settled.map((result) => result.status));
console.log(await Promise.any([fail('server 1 down', 50), ok('server 2', 100)]));
console.log(await Promise.race([ok('fast', 50), ok('slow', 150)]));
try {
await Promise.all([ok('menu', 100), fail('no stock', 50)]);
} catch (error) {
console.log('Promise.all failed:', error.message);
}▸ Ожидаемый результат
[ 'fulfilled', 'rejected' ] server 2 fast Promise.all failed: no stock
Не переставляя строки, добейся вывода в порядке 1, 2, 3, 4: оберни нужные строки в setTimeout(() => ..., 0) или Promise.resolve().then(() => ...).
console.log('4. task');
console.log('3. microtask');
console.log('1. sync');
console.log('2. sync');▸ Ожидаемый результат
1. sync 2. sync 3. microtask 4. task
Цены загружаются по очереди, а это медленно. Измени код так, чтобы все три запроса стартовали одновременно: используй Promise.all и деструктуризацию массива. Порядок загрузки изменится, а сумма останется прежней.
const prices = { tea: 2, bread: 1, jam: 5 };
function getPrice(item, ms) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`loaded ${item}`);
resolve(prices[item]);
}, ms);
});
}
// sequential and slow: rewrite with Promise.all
const tea = await getPrice('tea', 300);
const bread = await getPrice('bread', 100);
const jam = await getPrice('jam', 200);
console.log('Total:', tea + bread + jam);▸ Ожидаемый результат
loaded bread loaded jam loaded tea Total: 8
Главное
- У JavaScript один стек вызовов: в каждый момент работает только один фрагмент кода, а долгая синхронная работа замораживает всё.
- Таймеры и события попадают в очередь задач, колбэки промисов и код после
await— в очередь микрозадач. - Порядок: синхронный код → все микрозадачи → одна задача → снова все микрозадачи.
- В
async-функции код до первогоawaitвыполняется синхронно. - Независимые задачи запускай вместе и жди через
Promise.all/allSettled;raceиanyберут первый результат.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);?