Промисы раньше таймеров
Вернёмся на кухню. У повара две стопки записок. В первой — новые заказы, их он берёт по одному. Во второй — короткие доделки к тому, что он только что приготовил: посолить суп, положить вилку. Закончив очередное дело, повар сначала разбирает все доделки и только потом берёт следующий заказ.
Колбэк таймера и колбэк then тоже ждут, пока закончится текущий код. Когда ждут оба, первым выполняется колбэк промиса, даже если таймер поставлен раньше. Давайте посмотрим:
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
// timeoutPromise.resolve(1) создаёт промис, который уже выполнен со значением 1. Сначала печатаются обе обычные строки, затем оба шага цепочки, и только потом колбэк таймера с задержкой 0.
Две очереди
Готовые колбэки ждут не в одной очереди, а в двух:
- колбэки
thenиcatchпопадают в очередь микрозадач — это доделки повара; - колбэки
setTimeoutпопадают в очередь макрозадач — это новые заказы.
В спецификации макрозадачи называют задачами, поэтому в статьях встречаются оба слова.
Колбэк встаёт в очередь микрозадач, только когда его промис выполнен или отклонён; пока промис pending, колбэк ждёт в стороне. Поэтому колбэк then у промиса, который выполнит таймер, не обгонит сам этот таймер.
Правило событийного цикла
Событийный цикл работает по одному правилу. Когда текущий код закончился, цикл выполняет все микрозадачи и только потом берёт одну макрозадачу. Микрозадача, которая добавилась, пока цикл выполнял микрозадачи, тоже выполняется сразу, до следующей макрозадачи.
В примере это выглядит так:
- Пока работает текущий код, печатаются
startиend. Таймер заведён, а колбэк первогоthenуже стоит в очереди микрозадач. - Цикл выполняет первый
then: тот печатаетthen 1и возвращает2. - От этого в очередь микрозадач встаёт второй
then, и он тоже выполняется. - Микрозадач больше нет, и только теперь цикл берёт колбэк таймера.
Пройдите эту программу по шагам в виджете. Следите, когда стек вызовов пустеет и какой колбэк цикл берёт следующим.
1console.log("start");2setTimeout(() => console.log("timeout"), 0);3 4Promise.resolve(1)5 .then((v) => {6 console.log("then", v);7 return v + 1;8 })9 .then((v) => console.log("then", v));10console.log("end");
Текущий код печатает start.
После каждой макрозадачи цикл снова выполняет все микрозадачи, а затем берёт следующую макрозадачу. Задержка таймера здесь не важна: даже 0 не обгонит колбэк then, который уже ждёт.
Здесь аналогия с поваром перестаёт работать. Уставший повар может отложить доделки и взять новый заказ, а цикл так не делает никогда. Если микрозадачи без конца добавляют новые микрозадачи, таймер не дождётся своей очереди.
Это классический вопрос на собеседовании: что выведет код, где есть console.log, setTimeout(…, 0) и Promise.resolve().then(…)? Ответ строится по правилу: сначала весь текущий код, затем все микрозадачи, затем макрозадача таймера.
Уже выполненный промис
Колбэк then никогда не запускается на месте, даже у промиса, который уже выполнен. Вызов then у такого промиса только ставит колбэк в очередь, а текущий код идёт дальше:
const p = Promise.resolve("ready");
p.then((v) => console.log(v));
console.log("sync");
// sync
// readyЗначение у p есть с первой строки, но ready печатается вторым. Порядок всегда один: сначала весь текущий код, затем микрозадачи, затем макрозадачи таймеров.
Главное
- Колбэки
thenиcatchждут в очереди микрозадач, колбэкиsetTimeout— в очереди макрозадач. - Колбэк встаёт в очередь микрозадач, только когда его промис выполнен или отклонён.
- Когда текущий код закончился, цикл выполняет все микрозадачи и только потом одну макрозадачу.
- Даже у выполненного промиса колбэк
thenждёт конца текущего кода.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.