Promise — это отличительная особенность JavaScript как асинхронного языка программирования. Нравится вам это или нет, понять его в любом случае придется.

В этой статье я привожу 10 примеров кода с Promise, начиная от базового уровня заканчивая продвинутым. Готовы? Начнем!

Задача №1: Конструктор Promise

Каким будет вывод этого фрагмента кода?

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})

console.log('end');

Анализ

С первой задачей справиться легко.

Что мы знаем:

  • Блоки синхронного кода всегда выполняются последовательно сверху вниз.

  • Когда мы вызываем new Promise(callback), функция коллбэка будет выполнена сразу же.

Результат

Итак, этот код должен последовательно выводить start, 1, end.

console.log('start')

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})

console.log('end');

Задача №2: .then()

Каким будет вывод этого фрагмента кода?

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
})

promise1.then(res => {
  console.log(res)
})

console.log('end');

Анализ

Это фрагмент асинхронного кода. То есть коллбэк-функция в .then().

Помните о том, что интерпретатор JavaScript всегда сначала выполняет синхронный код, а затем асинхронный.

При столкновении с этой проблемой нам нужно только различать синхронный и асинхронный код.

Результат

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
})

promise1.then(res => {
  console.log(res)
})

console.log('end');;

Итак, выводом будет start , 1 , end и 2 .

Задача №3: resolve()

Каким будет вывод этого фрагмента кода?

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
  console.log(3)
})

promise1.then(res => {
  console.log(res)
})

console.log('end');

Анализ

Этот фрагмент кода почти такой же, как и предыдущий; единственная разница в том, что после resolve(2) есть console.log(3).

Помните, что метод resolve не прерывает выполнение функции. Код, стоящий за ним, по-прежнему будет выполняться.

Результат

Таким образом, выходным результатом будет start , 1 , 3 , end и 2 .

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
  console.log(3)
})

promise1.then(res => {
  console.log(res)
})

console.log('end');;

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

Задача №4: resolve() не вызывается

Каким будет вывод этого фрагмента кода?

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})

promise1.then(res => {
  console.log(2)
})

console.log('end');

Анализ

В этом коде метод resolve никогда не вызывался, поэтому promise1 всегда находится в состоянии ожидания (pending). Так что promise1.then(…) никогда не выполнялся. 2 не выводится в консоли.

Результат

Выходным результатом станет start , 1 , end .

console.log('start');

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})

promise1.then(res => {
  console.log(2)
})

console.log('end');;

Задача №5: Нечто, сбивающее с толку

console.log('start')

const fn = () => (new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
}))

console.log('middle')

fn().then(res => {
  console.log(res)
})

console.log('end')

Каким будет вывод этого фрагмента кода?

Анализ

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

Пожалуйста, помните, что независимо от того, сколько существует слоев вызовов функций, наши базовые принципы остаются неизменными:

  • Сначала выполняется синхронный код, а затем асинхронный.

  • Синхронный код выполняется в том порядке, в котором он был вызван.

Результат

Выходным результатом будет start , middle, 1 , end и success.

console.log('start')

const fn = () => (new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
}))

console.log('middle')

fn().then(res => {
  console.log(res)
})

console.log('end');

Задача №6: с Fulfilling Promise

Каким будет вывод этого фрагмента кода?

console.log('start')

Promise.resolve(1).then((res) => {
  console.log(res)
})

Promise.resolve(2).then((res) => {
  console.log(res)
})

console.log('end')

Анализ

Здесь Promise.resolve(1) вернет объект Promise, состояние которого fulfilled, а результат равен 1 . Это синхронный код.

 

Выходным результатом будет start , end , 1 и 2.

console.log('start')

Promise.resolve(1).then((res) => {
  console.log(res)
})

Promise.resolve(2).then((res) => {
  console.log(res)
})

console.log('end');

Ну что, думаете, это незначительные трудности?

Это только начало. Сложность Promise проявляется, когда он используется с setTimeout. Следующие задачи будут сложнее.

Готовы? Продолжим.

Задача №7: setTimeout vs Promise

Каким будет вывод этого фрагмента кода?

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
})

Promise.resolve().then(() => {
  console.log('resolve')
})

console.log('end')

Анализ

Обратите внимание, это сложный вопрос. Если вы сможете правильно ответить на него и объяснить причину, то можно считать, что ваше понимание асинхронного программирования в JavaScript достигло среднего уровня.

Прежде чем я дам объяснение, давайте вспомним соответствующую теоретическую базу.

Ранее мы говорили, что синхронный код выполняется в порядке вызова, так в каком же порядке выполняются эти асинхронные коллбэк-функции?

Кто-то может сказать, что тот, кто закончит первым, будет и выполнен первым. Что ж, это правда, но что, если две асинхронные задачи выполняются одновременно?

Например, в приведенном выше коде таймер setTimeout равен 0 секундам, а Promise.resolve() также вернет выполненный объект Promise сразу же после выполнения.

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

Некоторые джуны могут сказать, что setTimeout находится в начале, поэтому сначала будет выведен setTimeout, а затем resolve. На самом деле, это утверждение неверно.

Мы знаем, что многие вещи НЕ выполняются в порядке по принципу «первым пришел — первым вышел», например, трафик.

Приоритет

Обычно мы делим весь транспорт на две категории:

  • Общие транспортные средства.

  • Транспортные средства для чрезвычайных ситуаций. Например, пожарные машины и машины скорой помощи.

Чтобы проехать многолюдные перекрестки, мы пропустим первыми пожарные машины и машины скорой помощи. Автомобили скорой помощи имеют приоритет выше, чем другой транспорт. Ключевое слово: приоритеты.

 В JavaScript EventLoop также есть понятие приоритета.

  • Задачи с более высоким приоритетом называются микрозадачами. Например: Promise, ObjectObserver, MutationObserver, process.nextTick, async/await.

  • Задачи с более низким приоритетом называются макрозадачами. Например: setTimeout, setInterval и XHR.

Хотя setTimeout и Promise.resolve() выполняются одновременно, и даже код setTimeout еще впереди, но из-за низкого приоритета относящаяся к нему коллбэк-функция выполняется позже.

 

Результат

Выходным результатом будет start , end , resolve и setTimeout.

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
})

Promise.resolve().then(() => {
  console.log('resolve')
})

console.log('end');

Задача №8: Микрозадачи смешиваются с макрозадачами

Каким будет вывод этого фрагмента кода?

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});

promise.then((res) => {
  console.log(res);
});

console.log(4);

Анализ

Эту задачу легко выполнить, если вы поняли предыдущий код.

Нам просто нужно выполнить эти три шага:

  1. Найти синхронный код.

  2. Найти код микрозадачи.

  3. Найти код макрозадачи.

Сначала выполните синхронный код:

Выведется 1 , 2  и 4 .

Затем выполните микрозадачу:

 Но вот ловушка: поскольку текущий Promise все еще находится в состоянии ожидания (pending), код в данный момент выполняться не будет.

Затем выполните макрозадачу:

 

И состояние promise становится fulfilled .

Затем с помощью Event Loop снова выполните микрозадачу:

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => {
  console.log(res);
});
console.log(4);;

Задача №9: приоритезировать микрозадачи и макрозадачи

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

Что выводит этот фрагмент кода?

const timer1 = setTimeout(() => {
  console.log('timer1');
  
  const promise1 = Promise.resolve().then(() => {
    console.log('promise1')
  })
}, 0)

const timer2 = setTimeout(() => {
  console.log('timer2')
}, 0)

Анализ

Некоторые могут подумать, что микрозадачи и макрозадачи выполняются так:

  1. Сначала выполняются все микрозадачи.

  2. Выполняются все макрозадачи.

  3. Выполняются все микрозадачи снова.

  4. Цикл повторяется / Цикл завершается.

Но это утверждение неверно. Правильно вот так:

  1. Сначала выполняются все микрозадачи.

  2. Выполняется одна макрозадача.

  3. Повторно выполняются все (вновь добавленные) микрозадачи.

  4. Выполняется следующая макрозадача.

  5. Цикл повторяется / Цикл завершается.

Так:

Или вот так:

Таким образом, в приведенном выше коде коллбэк-функция Promise.then будет выполняться перед коллбэк-функцией второго setTimeout, потому что это микрозадача, и она была врезана в последовательность задач.

 

Результат

 

Задача №10: типичный вопрос с собеседования

Что ж, это наша последняя задача. Если вы сможете правильно определить вывод этого кода, то ваше понимание Promise уже на высоком уровне. И однотипные вопросы на собеседовании точно не станут для вас трудностью.

Что выводит этот фрагмент кода?

console.log('start');

const promise1 = Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
});

const timer1 = setTimeout(() => {
  console.log('timer1')
  const promise2 = Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)

console.log('end');

Анализ

Эта задача является более суровой версией предыдущей задачи, но основной принцип остается прежним.

Вспомните, что мы узнали ранее:

  1. Синхронный код

  2. Все микрозадачи

  3. Первая макрозадача

  4. Все недавно добавленные микрозадачи

  5. Следующая макрозадача 

Итак:

  1. Выполним весь синхронный код:

  1. Выполним все микрозадачи:

  1. Выполним первую макрозадачу:

 

Примечание. На этом шаге макрозадача добавляет в очередь задач новую микрозадачу.

4. Выполним все вновь добавленные микрозадачи:

5. Выполним следующую макрозадачу:

Результат

Вывод будет таким.

console.log('start');

const promise1 = Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
});

const timer1 = setTimeout(() => {
  console.log('timer1')
  const promise2 = Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)

console.log('end');;

Заключение

Для всех подобных вопросов нужно просто запомнить три правила:

  1. Интерпретатор JavaScript всегда сначала выполняет синхронный код, а затем асинхронный.

  2. Микрозадачи имеют приоритет над макрозадачами.

3. Микрозадачи могут врезаться в последовательность выполнения в Event Loop.


Завтра состоится открытый урок «Флексы и гриды — в чем отличие?». На вебинаре на практике рассмотрим два популярные способа расположения элементов макета. Рассмотрим их плюсы и минусы, разные примеры применения. Регистрация по ссылке.

Комментарии (15)


  1. DDroll
    06.09.2022 19:18
    +6

    Хороший материал для улучшения понимания промисов и асинхронного выполнения. Но… Собеседования? Вот кто мне может объяснить смысл таких вопросов на собеседовании? Что вы хотите выяснить о кандидате такими вопросами? Это как на собеседовании пилота задавать ему вопросы о технологических особенностях приготовления сплава обшивки крыла, по моему мнению. Вот как эти викторинные вопросы собесов (что вернет этот бредовый говнокод, за который тебя уволят, если ты напишешь его в коммит) коррелируют с реальными задачами фомошлепства?


    1. faiwer
      06.09.2022 22:01

      В теории это ценный навык для nodejs backend разработчиков. Там ещё и не такое бывает нужно (к примеру async hooks или node fibers).


      В целом про microTasks лучше знать, когда речь заходит о каких-нибудь асинхронных числодробилках. Как и о штуках вроде requestIdleCallback, requestAnimatonFrame


      1. BuccapuoH
        06.09.2022 22:34

        По моему опыту - никто даже на бэкенде в такие низкоуровневые дебри не лезет. Большинство использует сторонние пакеты, которые это все инкапсулируют. Спорить не буду что есть какие-то конторы где нужен низкоуровневый код, только вот думаю их сильно меньше, чем любителей задавать такие вопросы на собеседованиях.


        1. faiwer
          06.09.2022 23:45

          в такие низкоуровневые дебри не лезет

          Это не низкоуровневые дебри. Это то как раз на поверхности. По сути всё крутится вокруг того как работает event-loop. Статью не читал, 10 из 10 ответил правильно. Просто зная логику, которая стоит за этим. Promise.then выполнится в приоритетной очереди (в отличии от setTimeout), new Promise body исполняется синхронно, а .then на следующем тике micro-queue. Дальше просто внимательность.


          Большинство использует сторонние пакеты, которые это все инкапсулируют

          Вы о чём? Зачем вам обёртки над process.nextTick()? Что вы там инкапсулировать собрались?


          только вот думаю их сильно меньше, чем любителей задавать такие вопросы на собеседованиях

          Это всегда так. Но если вам нужна 3х зарплата, лучше быть готовым (и в правильные места собеседоваться).


          Ей богу, одно дело жаловаться на тупые вопросы вида [] + {} + NaN === ?, а совсем другое на базу в области event loop, который вроде как святая святых.


          Да и допустим спросят такое, скажем, на позицию "middle формошлёпер". Ну не ответит человек, предложат на 10% ниже зарплату. Ой беда. Такого рода вопросы нужны чтобы прощупать глубину ваших знаний. Чтобы было понимание какого рода задачи вы можете закрыть без участия более опытных товарищей.


          Чем больше головняка человек может снять тем больше ему готовы платить. Умеет в soft skills и управление командой? Отлично. Знает как отлаживать удалённые nodejs процессы наживую? Прекрасно. Может на ранних этапах на code review зарезать неоптимальный BigO и предложить альтернативу? Чудесно! Специалист в области обнаружения утечек памяти? Может устроить мастер класс в области безопасности? Большой специалист в кросс-браузерной вёрстке? Ну и т.д..


          1. BuccapuoH
            07.09.2022 10:57

            Понимаю, что у вас другой опыт. Но я нигде не видел, чтобы кто-то использовал process.nextTick() напрямую. Тоже самое про requestAnimationFrame. Вполне возможно, что как раз тот самый формошлёп, о котором вы говорите.


            1. faiwer
              07.09.2022 11:44

              Другой опыт. В текущем проекте:


              • фронт, поиск по "requestAnimationFrame": 36 results
              • бек, поиск по "process.nextTick": 26 results


              1. BuccapuoH
                07.09.2022 12:49

                Если не секрет, что за проект и для чего используете эти API?


                1. faiwer
                  07.09.2022 13:26

                  Т.к. я 99% времени работаю с фронтом, то могу про requestAnimationFrame рассказать. В первую очередь это нужно для плавных анимаций. Будь то <canvas/>, <svg/> или что-то сделанное вручную. В целом идея в том, что это что-то визуальное и в идеале должно работать без видимых лагов, но при этом не перегружать CPU бесполезными вычислениями.


                  requestIdleCallback нужен когда нужно сделать что-то не супер важное, в lazy режиме, но так чтобы не в ущерб чему-то важному. Например подгрузить какую-нибудь второстепенную библиотеку.


                  Всякие microTask-и это, как мне кажется, всегда что-то сильно хитрое. Например нужно сделать что-то асинхронно, но как можно раньше. Скажем если у вас нагруженное nodejs приложение, то какой-нибудь setTimeout(..., 0) может запустить свой callback далеко не сразу, т.к. по пути будет 100500 promise-ов с их .then. Но можно вручную libuv заставить что-то запустить сразу первоочерёдно, но асинхронно. Полагаю в условных играх, где latency очень важен, это очень полезный инструмент. Ну или если у вас есть нужда в асинхрощине, но при этом вы используете локи редиса и не желаете его держать больше чем минимально необходимо. В общем всякое бывает.


                  1. strannik_k
                    07.09.2022 22:35
                    -1

                    Думаю, BuccapuoH, как и я, хотел узнать, что у вас за проект такой, в котором понадобилось столько раз использовать requestAnimationFrame и process.nextTick) И была ли в этом необходимость или же это просто преждевременная оптимизация.


                    1. faiwer
                      07.09.2022 23:24

                      Крупные SPA на 1M+ LOC. С очень разнообразной функциональностью. В том числе и графика (svg, карты на canvas, отрисовка растра), анимация.


                      И была ли в этом необходимость или же это просто преждевременная оптимизация.

                      Ну это надо каждый отдельно взятый случай исследовать под микроскопом. Но я с трудом представляю как requestAnimationFrame может быть преждевременной оптимизацией (как и оптимизацией вообще). Это как вилка и ложка. Нужно либо то, либо это. Каждой задаче свой инструмент. Вилкой есть суп просто глупо, хотя при некоторой доле сноровки возможно.


                      1. strannik_k
                        08.09.2022 00:08

                        Про оптимизацию я глупость написал, так как ни разу не приходилось использовать requestAnimationFrame.

                        После прочтения фраз вроде "не перегружать CPU бесполезными вычислениями" в голове почему-то засела мысль, что вы ее возможно для каких-то оптимизаций используете.


      1. Lexicon
        07.09.2022 14:13

        Я бы сказал, что знать особенности движка, с которым работаешь, — ценный навык для любых разработчиков. Вот только задачки на интервью такого рода зачем? Разве они его проверяют? До этого был чувак с таблицей, теперь с машинками, чтобы не запутаться

        Задачки в принципе зло, но на промайзы, пожалуй, — самые бесполезные


        1. faiwer
          07.09.2022 16:47

          Вот только задачки на интервью такого рода зачем? Разве они его проверяют?

          Не понимаю почему нет.


          Задачки в принципе зло

          Не согласен.


    1. ivegner
      07.09.2022 15:31
      +1

      Хороший материал для улучшения понимания промисов и асинхронного выполнения

      А мне он не показался хорошим. По-моему, он всё запутывает попытками обойти понятие очереди заданий и представить код обладающий какими-то статическими характеристиками. Какой ещё синхронный, какой асинхронный код? Код везде один и тот же, и выполняется он императивно в одном потоке безо всякой "одновременности". Просто некоторые вызовы (setTimeout, promise.then) добавляют переданные им замыкания в очередь заданий, которая выполняется в порядке задержки (таймаута) и приоритета. Раскраски и диаграммы, предложенные автором оригинальной заметки, по-моему, в десять раз сложнее, чем нужно.


    1. DarthVictor
      07.09.2022 19:13

      Ну понимать как работает fetch внутри set setInterval / setTimeout пожалуй все таки стоит. А это сразу и таски / микротаски и даже возможное состояние гонки.