JavaScript · Синтаксис · Продвинутый уровень
Цикл событий
Как JavaScript обрабатывает конкурентность с одним потоком и одной очередью. Охватывает стек вызовов, очередь задач, очередь микрозадач и порядок `setTimeout` vs `Promise.resolve`.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Примеры порядка, async/await, микрозадача в макрозадаче, предсказание
#Таблица справочника, ловушки, await в цикле, Web Workers
#Упражнения:
Предсказать: синхронный стек вызовов
#Не запуская, предскажите точный порядок вывода: ```js 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');
// Ожидаемый вывод (запишите до запуска):
// ???
Решение
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) выполняется после синхронного кода
#Предскажите порядок вывода и объясните ПОЧЕМУ `'2'` появляется после `'3'`, хотя задержка 0: ```js console.log('1'); setTimeout(() => console.log('2'), 0); console.log('3'); ```
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() — это микрозадача
#Предскажите вывод. Затем сравните с примером `setTimeout(0)` из предыдущего упражнения и объясните разницу: ```js console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3'); ```
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
#Предскажите точный вывод (4 строки). Это канонический тест на порядок цикла событий: ```js console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D'); ```
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
#Предскажите вывод: ```js 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');
// Вывод: ???
Решение
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
#Предскажите вывод: ```js 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');
// Вывод: ???
Решение
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 — явное планирование микрозадачи
#Используйте `queueMicrotask` для планирования коллбэка и предскажите, где он появится относительно синхронного кода и `setTimeout(0)`: ```js setTimeout(() => console.log('timeout'), 0); queueMicrotask(() => console.log('microtask')); console.log('sync'); ```
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'.
Блокировка цикла событий
#Напишите функцию `blockFor(ms)`, синхронно блокирующую главный поток на `ms` миллисекунд через busy loop. Запланируйте `setTimeout` на 50мс, затем вызовите `blockFor(200)`. Наблюдайте, что таймаут срабатывает поздно — ~200мс, не 50мс — и объясните почему.
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`: ```js 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);
// Вывод: ???
Решение
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
Полный тест на порядок
#Самый сложный вариант — предскажите все 8 строк вывода: ```js 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'); ```
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)