Урок 06

Event loop подробно

Event loop — механизм, позволяющий однопоточному JavaScript выполнять асинхронные операции.

Компоненты

  • Call stack — стек вызовов функций
  • Web APIs — таймеры, сеть, DOM (в браузере)
  • Microtask queue — очередь микрозадач
  • Macrotask queue — очередь макрозадач
  • Event loop — перекладывает задачи

Порядок выполнения

  1. Синхронный код выполняется до конца
  2. Все микрозадачи
  3. Одна макрозадача
  4. Снова все микрозадачи
  5. Следующая макрозадача
  6. И так далее

Микрозадачи

  • Promise.then, catch, finally
  • queueMicrotask
  • MutationObserver
Promise.resolve().then(() => console.log('микрозадача'));

Макрозадачи

  • setTimeout, setInterval
  • setImmediate (Node.js)
  • События DOM
  • I/O
setTimeout(() => console.log('макрозадача'), 0);

Классический пример

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// 1, 4, 3, 2

Разбор:

  1. Синхронный код: 1, 4
  2. Микрозадачи: 3
  3. Макрозадачи: 2

Промисы всегда раньше таймеров.

Ещё пример

setTimeout(() => console.log('timeout 1'), 0);
setTimeout(() => console.log('timeout 2'), 0);

Promise.resolve()
    .then(() => console.log('promise 1'))
    .then(() => console.log('promise 2'));

console.log('sync');

// sync
// promise 1
// promise 2
// timeout 1
// timeout 2

Микрозадачи добавляют новые микрозадачи

Promise.resolve()
    .then(() => {
        console.log('1');
        Promise.resolve().then(() => console.log('2'));
    })
    .then(() => console.log('3'));

// 1, 3, 2

2 добавляется в конец очереди микрозадач, 3 уже там ждала.

Микрозадачи могут блокировать

Бесконечная цепочка микрозадач блокирует макрозадачи и UI:

function loop() {
    Promise.resolve().then(loop);
}
loop(); // браузер зависнет

Макрозадачи так не блокируют, потому что между ними браузер может отрисовать кадр.

setTimeout(0)

Не гарантирует немедленное выполнение. Минимум ~4мс в браузере, ещё больше после вложенных таймеров.

setTimeout(() => console.log('не сразу'), 0);

requestAnimationFrame

Отдельная очередь — перед отрисовкой кадра:

requestAnimationFrame(() => console.log('перед кадром'));

Порядок: микрозадачи → requestAnimationFrame → отрисовка → макрозадачи.

Node.js event loop

У Node.js свои фазы:

  • timers — setTimeout, setInterval
  • pending callbacks — I/O-ошибки
  • poll — I/O
  • check — setImmediate
  • close callbacks — закрытие

setImmediate выполняется после poll, setTimeout — в фазе timers. Порядок может отличаться.

Микрозадачи между фазами

После каждой фазы Node.js очищает микрозадачи, потом идёт к следующей.

Практика: тяжёлые задачи

Тяжёлое вычисление блокирует поток:

function heavy() {
    for (let i = 0; i < 1e9; i++) {}
}

heavy(); // UI зависнет

Решения:

  • Web Workers — отдельный поток
  • Разбить на части через setTimeout
  • requestIdleCallback — в свободное время

Итоги

  • Event loop: стек → микрозадачи → макрозадачи
  • Промисы раньше таймеров
  • Микрозадачи могут блокировать
  • requestAnimationFrame перед отрисовкой
  • Node.js имеет свои фазы
  • Тяжёлые вычисления — в Web Workers