JavaScript · Синтаксис · Продвинутый уровень
Цикл событий
Как JavaScript обрабатывает конкурентность с одним потоком и одной очередью. Охватывает стек вызовов, очередь задач, очередь микрозадач и порядок `setTimeout` vs `Promise.resolve`.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Примеры порядка, async/await, микрозадача в макрозадаче, предсказание
#Таблица справочника, ловушки, await в цикле, Web Workers
#Упражнения:
Предсказать: синхронный стек вызовов
#function a() { console.log('a'); b(); }
function b() { console.log('b'); }
console.log('start');
a();
console.log('end');
// Ожидаемый вывод (запишите до запуска):
// ???
Решение
function a() { console.log('a'); b(); }
function b() { console.log('b'); }
console.log('start');
a();
console.log('end');
// Вывод: start, a, b, end
// Стек: main -> a -> b, затем разворачивается: b, a, main
setTimeout(0) выполняется после синхронного кода
#console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Порядок вывода: ???
// Почему?
Решение
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Вывод: 1, 3, 2
//
// setTimeout ставит macrotask в очередь задач.
// Текущий синхронный код (1, затем 3) завершается первым.
// Только потом цикл событий берёт коллбэк из очереди (2).
Promise .then() — это микрозадача
#console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');
// Вывод: ???
// Чем отличается от setTimeout(fn, 0)?
Решение
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');
// Вывод: 1, 3, 2
// Тот же порядок, что и с setTimeout, но механизм разный.
//
// Promise .then() — МИКРОЗАДАЧА.
// Микрозадачи выполняются после текущей задачи, но ДО следующего macrotask.
// setTimeout: sync -> macrotask
// .then(): sync -> microtask -> (затем macrotask если есть)
Классический порядок: sync + Promise + setTimeout
#console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Вывод (4 строки): ???
Решение
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Вывод: A, D, C, B
//
// Шаг 1 - sync: A, macro(B) в очередь, micro(C) в очередь, D
// Шаг 2 - вычерпать микрозадачи: C
// Шаг 3 - следующий macrotask: B
Цепочка .then() и чередование с setTimeout
#setTimeout(() => console.log('macro'), 0);
Promise.resolve()
.then(() => console.log('micro 1'))
.then(() => console.log('micro 2'))
.then(() => console.log('micro 3'));
console.log('sync');
// Вывод: ???
Решение
setTimeout(() => console.log('macro'), 0);
Promise.resolve()
.then(() => console.log('micro 1'))
.then(() => console.log('micro 2'))
.then(() => console.log('micro 3'));
console.log('sync');
// Вывод: sync, micro 1, micro 2, micro 3, macro
//
// Каждый .then() в цепочке добавляет микрозадачу при разрешении предыдущей.
// Вся очередь микрозадач вычерпывается до того, как 'macro' получит шанс.
async/await приостанавливается на каждом await
#async function main() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
console.log('before');
main();
console.log('after');
// Вывод: ???
Решение
async function main() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
console.log('before');
main();
console.log('after');
// Вывод: before, async start, after, async end
//
// main() выполняется синхронно до первого 'await'.
// 'await' приостанавливает main() и планирует остаток как микрозадачу.
// Выполнение возвращается на место вызова: выполняется 'after'.
// Затем очередь микрозадач: выполняется 'async end'.
queueMicrotask — явное планирование микрозадачи
#setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Вывод: ???
Решение
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Вывод: sync, microtask, timeout
//
// queueMicrotask() планирует микрозадачу, как и Promise.resolve().then().
// Микрозадачи выполняются до любого macrotask, поэтому 'microtask' опережает 'timeout'.
Блокировка цикла событий
#function blockFor(ms) {
// busy loop: заблокируйте поток на ms миллисекунд
}
const t0 = Date.now();
setTimeout(() => {
console.log(`Сработал через ${Date.now() - t0}мс`); // ожидается ~200мс
}, 50);
blockFor(200);
console.log(`Sync завершён за ${Date.now() - t0}мс`);
Решение
function blockFor(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const t0 = Date.now();
setTimeout(() => {
console.log(`Сработал через ${Date.now() - t0}мс`); // ~200мс
}, 50);
blockFor(200);
console.log(`Sync завершён за ${Date.now() - t0}мс`);
// Пока blockFor() выполняется, цикл событий застрял.
// Никакие таймеры, сеть, события не могут сработать.
// Таймаут на 50мс может сработать только после освобождения потока.
Микрозадача внутри macrotask
#setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);
// Вывод: ???
Решение
setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);
// Вывод: macro 1, micro inside, macro 2
//
// Цикл событий: выполняет ОДИН macrotask -> вычерпывает ВСЕ микрозадачи -> следующий macrotask.
// То есть: macro 1 -> .then() ставит микрозадачу -> micro inside -> macro 2
Полный тест на порядок
#console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');
// Вывод (8 строк): ???
Решение
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');
// Вывод: 1, 8, 3, 7, 5, 2, 6, 4
//
// Фаза 1 sync: 1, macro(2), micro(первый .then), macro(6), micro(7), 8
// Фаза 2 micro: первый .then -> 3, macro(4), micro(5)
// micro(7) -> 7
// micro(5) -> 5
// Фаза 3 macro: 2, 6, 4 (порядок FIFO)