JavaScript · Синтаксис · Продвинутый уровень

Цикл событий

10 задач

Как JavaScript обрабатывает конкурентность с одним потоком и одной очередью. Охватывает стек вызовов, очередь задач, очередь микрозадач и порядок `setTimeout` vs `Promise.resolve`.

Почему однопоточный, стек вызовов, очередь задач, очередь микрозадач

#
**Почему JavaScript однопоточный** JavaScript разрабатывался для браузера где он касается DOM. Если бы два потока могли изменять DOM одновременно — нужны были бы блокировки. Однопоточность избегает этого — нет гонок данных, дедлоков, мьютексов. Конкурентность достигается иначе: *не блокируя* на I/O. Когда браузер делает сетевой запрос — он передаёт его ОС и *регистрирует callback* который будет вызван когда придут данные. Тем временем JS-поток свободен обрабатывать UI-события, запускать анимации. **Модель цикла событий** Есть один стек вызовов. Он выполняет один фрейм за раз до завершения. Callbacks и микрозадачи ставятся в очередь и обрабатываются между фреймами: ``` Стек вызовов Очередь микрозадач Очередь задач (Макрозадачи) ────────── ─────────────── ────────────────────── │ fn() │ ←── │ .then() │ ←─ │ setTimeout cb │ │ main() │ │ await │ │ I/O callback │ ────────── │ queueMicro │ │ setInterval cb │ ─────────────── ────────────────────── ``` **Полный цикл — шаг за шагом:** 1. Выполнить текущую задачу (синхронный код) до завершения 2. Полностью опустошить очередь микрозадач (новые микрозадачи добавленные во время опустошения тоже выполняются *до* следующей макрозадачи) 3. Рендеринг (только браузер, если пришло время фрейма) 4. Взять следующую макрозадачу из очереди задач 5. Повторить **Макрозадачи** (очередь задач): `setTimeout`, `setInterval`, I/O callbacks, UI-события **Микрозадачи** (очередь микрозадач): Promise `.then`/`.catch`/`.finally`, возобновление после `await`, `queueMicrotask()`, `MutationObserver`

Примеры порядка, async/await, микрозадача в макрозадаче, предсказание

#
**Примеры порядка выполнения** ```js // Паттерн: sync → микрозадачи → макрозадачи console.log('A'); // sync setTimeout(() => console.log('D'), 0); // макрозадача Promise.resolve().then(() => console.log('C')); // микрозадача console.log('B'); // sync // Вывод: A B C D ``` **async/await под капотом** `async/await` — синтаксический сахар над Promises. Каждый `await` создаёт границу микрозадачи — выполнение приостанавливается и возобновляется как микрозадача: ```js async function f() { console.log('A'); // выполняется синхронно при вызове f() await Promise.resolve(); console.log('C'); // возобновляется как микрозадача } f(); console.log('B'); // Вывод: A B C ``` **Микрозадача внутри макрозадачи** ```js setTimeout(() => { console.log('macro 1'); Promise.resolve().then(() => console.log('micro inside macro 1')); }, 0); setTimeout(() => console.log('macro 2'), 0); // Вывод: macro 1 → micro inside macro 1 → macro 2 ``` **Алгоритм предсказания вывода** ```js console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve() .then(() => console.log(3)) .then(() => console.log(4)); console.log(5); // Вывод: 1 5 3 4 2 // // Шаг 1 (sync): логирует 1, планирует setTimeout, цепочка .then×2, логирует 5 // Шаг 2 (микрозадачи): .then(3) → .then(4) // Шаг 3 (макрозадача): setTimeout → 2 ``` **`queueMicrotask(fn)`** ```js queueMicrotask(() => console.log('выполняется перед следующей макрозадачей')); ```

Таблица справочника, ловушки, await в цикле, Web Workers

#
**Краткий справочник** | Что | Очередь | Приоритет | |---|---|---| | Синхронный код | текущая задача | наивысший | | `Promise.then/catch/finally` | микрозадача | до следующей макрозадачи | | Возобновление после `await` | микрозадача | до следующей макрозадачи | | `queueMicrotask()` | микрозадача | до следующей макрозадачи | | `setTimeout(fn, 0)` | макрозадача | после всех микрозадач | | `setInterval` | макрозадача (повторяемая) | после всех микрозадач | | `requestAnimationFrame` | перед рендером | только браузер, ~16мс | **Ловушки** **1. Тяжёлая синхронная работа блокирует всё:** ```js // ПЛОХО — замораживает вкладку items.forEach(item => heavyWork(item)); // ЛУЧШЕ — уступать циклу событий между порциями async function processAll(items) { for (let i = 0; i < items.length; i++) { heavyWork(items[i]); if (i % 100 === 0) await new Promise(r => setTimeout(r, 0)); } } ``` **2. Бесконечная рекурсия микрозадач — морит голодом макрозадачи:** ```js // Замораживает вкладку: function loop() { Promise.resolve().then(loop); } // Безопасный вариант: function loop() { setTimeout(loop, 0); } ``` **3. `await` в цикле:** ```js // Последовательно — каждый fetch ждёт предыдущего: for (const url of urls) { const data = await fetch(url); } // Конкурентно — все fetch стартуют одновременно: const results = await Promise.all(urls.map(url => fetch(url))); ``` **Web Workers — настоящий параллелизм для CPU-работы** ```js // main.js const worker = new Worker('./worker.js'); worker.postMessage({ data: largeArray }); worker.onmessage = e => console.log('result:', e.data); // worker.js self.onmessage = e => { const result = heavyCPUWork(e.data.data); self.postMessage(result); }; ```
01

Предсказать: синхронный стек вызовов

#

Не запуская, предскажите точный порядок вывода: ```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
02

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).
03

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 если есть)
04

Классический порядок: 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
05

Цепочка .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' получит шанс.
06

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'.
07

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'.
08

Блокировка цикла событий

#

Напишите функцию `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мс может сработать только после освобождения потока.
09

Микрозадача внутри 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
10

Полный тест на порядок

#

Самый сложный вариант — предскажите все 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)