Перейти к содержимому
Теория · Асинхронность · урок 5 из 12

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

Вернёмся на кухню. У повара две стопки записок. В первой — новые заказы, их он берёт по одному. Во второй — короткие доделки к тому, что он только что приготовил: посолить суп, положить вилку. Закончив очередное дело, повар сначала разбирает все доделки и только потом берёт следующий заказ.

Колбэк таймера и колбэк then тоже ждут, пока закончится текущий код. Когда ждут оба, первым выполняется колбэк промиса, даже если таймер поставлен раньше. Давайте посмотрим:

JavaScript
console.log("start");
setTimeout(() => console.log("timeout"), 0);

Promise.resolve(1)
    .then((v) => {
        console.log("then", v);
        return v + 1;
    })
    .then((v) => console.log("then", v));
console.log("end");
// start
// end
// then 1
// then 2
// timeout

Promise.resolve(1) создаёт промис, который уже выполнен со значением 1. Сначала печатаются обе обычные строки, затем оба шага цепочки, и только потом колбэк таймера с задержкой 0.

Две очереди

Готовые колбэки ждут не в одной очереди, а в двух:

  • колбэки then и catch попадают в очередь микрозадач — это доделки повара;
  • колбэки setTimeout попадают в очередь макрозадач — это новые заказы.

В спецификации макрозадачи называют задачами, поэтому в статьях встречаются оба слова.

Колбэк встаёт в очередь микрозадач, только когда его промис выполнен или отклонён; пока промис pending, колбэк ждёт в стороне. Поэтому колбэк then у промиса, который выполнит таймер, не обгонит сам этот таймер.

Правило событийного цикла

Событийный цикл работает по одному правилу. Когда текущий код закончился, цикл выполняет все микрозадачи и только потом берёт одну макрозадачу. Микрозадача, которая добавилась, пока цикл выполнял микрозадачи, тоже выполняется сразу, до следующей макрозадачи.

В примере это выглядит так:

  1. Пока работает текущий код, печатаются start и end. Таймер заведён, а колбэк первого then уже стоит в очереди микрозадач.
  2. Цикл выполняет первый then: тот печатает then 1 и возвращает 2.
  3. От этого в очередь микрозадач встаёт второй then, и он тоже выполняется.
  4. Микрозадач больше нет, и только теперь цикл берёт колбэк таймера.

Пройдите эту программу по шагам в виджете. Следите, когда стек вызовов пустеет и какой колбэк цикл берёт следующим.

ВживуюСобытийный цикл: then раньше таймерашаг 1 из 9 · рамок: 1

Текущий код печатает start.

Колбэк начинается, только когда стек пуст

После каждой макрозадачи цикл снова выполняет все микрозадачи, а затем берёт следующую макрозадачу. Задержка таймера здесь не важна: даже 0 не обгонит колбэк then, который уже ждёт.

Здесь аналогия с поваром перестаёт работать. Уставший повар может отложить доделки и взять новый заказ, а цикл так не делает никогда. Если микрозадачи без конца добавляют новые микрозадачи, таймер не дождётся своей очереди.

Это классический вопрос на собеседовании: что выведет код, где есть console.log, setTimeout(…, 0) и Promise.resolve().then(…)? Ответ строится по правилу: сначала весь текущий код, затем все микрозадачи, затем макрозадача таймера.

Уже выполненный промис

Колбэк then никогда не запускается на месте, даже у промиса, который уже выполнен. Вызов then у такого промиса только ставит колбэк в очередь, а текущий код идёт дальше:

JavaScript
const p = Promise.resolve("ready");

p.then((v) => console.log(v));

console.log("sync");
// sync
// ready

Значение у p есть с первой строки, но ready печатается вторым. Порядок всегда один: сначала весь текущий код, затем микрозадачи, затем макрозадачи таймеров.

Главное

  • Колбэки then и catch ждут в очереди микрозадач, колбэки setTimeout — в очереди макрозадач.
  • Колбэк встаёт в очередь микрозадач, только когда его промис выполнен или отклонён.
  • Когда текущий код закончился, цикл выполняет все микрозадачи и только потом одну макрозадачу.
  • Даже у выполненного промиса колбэк then ждёт конца текущего кода.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом