Обещания в JS. Еще один вопрос на собеседовании

Уже есть множество статей, объясняющих, как промисы работают в JavaScript и почему так важно их понимать. Этот пост не предназначен для повторения теории еще раз, а для того, чтобы рассмотреть несколько сложных вопросов, которые вам могут задать на собеседовании.

Эта проблема

Недавно мне выдали такой код:

И вопросы были:

  • объясните порядок выполнения каждого варианта
  • что, если в цепочку будет добавлено больше предметов
  • какие обещания эквивалентны

Решение

Итак, начнем с написания возможной реализации функций foo и bar:

Как вы могли заметить, console.logs много, они должны помочь нам понять порядок выполнения в каждом случае. Кроме того, обратите внимание, что foo обещание разрешается через ~ 1 с, а bar - через ~ 3 секунды.

Теперь я хотел бы рассмотреть каждый случай отдельно и ответить на первые два вопроса. Чтобы решить второй вопрос (о более длинной цепочке обещаний), я собираюсь добавить второй then к каждому варианту.

Первый случай

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

Теперь, построчно, мы можем предсказать порядок, в котором будет выполняться код:

  1. Мы вызываем foo, который возвращает нам Promise .
  2. Затем внутри тела new Promise() мы вызываем setTimeout, который разрешит Promise за ~ 1 с.
  3. После выполнения Promise мы переходим к первому телу then с res равным "foo resolved" и вызываем bar.
  4. Затем мы повторяем шаги №1–3 с bar.
  5. Через ~ 3 с вызывается второе тело then с res равным "bar resolved".

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

foo
foo timeout before
foo timeout after
foo timeout                 // after ~1s
inside then 1: foo resolved // after ~1s
bar
bar timeout before
bar timeout after
bar timeout                 // after ~3s
inside then 2: bar resolved // after ~3s

Вот и все! Должно быть довольно просто, если вы раньше работали с Promises.

Второй случай

Случай №2 абсолютно идентичен предыдущему и, по сути, это всего лишь ярлык. Вот почему я пропущу это. Однако вы можете запустить код и сравнить результат с номером 1.

Третий случай

А теперь самый нетривиальный случай (по крайней мере, для меня):

Честно говоря, я не знал, что именно произойдет, если bar вернет что-то, кроме функции. Итак, мне пришлось проверить MDN:

Если один или оба аргумента опущены или предоставлены без функций, тогда then будет отсутствовать обработчик (-ы), но не будет генерировать никаких ошибок. Если Promise, который вызывается then, принимает состояние (fulfillment или rejection), для которого then не имеет обработчика, создается новый Promise без дополнительных обработчиков, просто принимая конечное состояние исходного Promise, в котором был вызван then.

Итак, когда bar возвращает функцию, это будет обработчик для then. В противном случае этот then будет пропущен, а следующий в цепочке получит результат выполнения foo.

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

  1. Мы вызываем foo, который возвращает Promise .
  2. Сразу после этого мы вызываем bar, который возвращает Promise.
  3. После выполнения Promise из foo (через ~ 1 с) мы переходим к первому then. Однако bar вернул Promise, а не функцию, поэтому мы пропускаем его и переходим сразу ко второму then.
  4. Ввод для второго then поступает непосредственно из foo, потому что мы пропустили 1-й then.
  5. Спустя ~ 3 секунды ошибка bar: Promise была решена.

В итоге мы получим следующий результат:

foo
foo timeout before
foo timeout after
bar
bar timeout before
bar timeout after
foo timeout                 // after ~1s
inside then 2: foo resolved // after ~1s
bar timeout                 // after ~3s

Четвертый случай

Этот случай очень похож на первый, за исключением одной детали - мы не возвращаем результат bar() в первом then:

И эта тонкая разница все меняет:

  1. Мы вызываем foo, который возвращает Promise .
  2. Через ~ 1 с он разрешается, и мы входим в обработчик firstthen.
  3. Там мы вызываем функцию bar. Однако, как только мы вернем Promise из функции, мы перейдем непосредственно ко второму then. Мы не ждем выполнения bar’s Promise.
  4. Поскольку у нас нет явного return оператора в первом then обработчике, мы возвращаем undefined, который становится входом для второго обработчика then.
  5. Через ~ 3 с мы разрешили bar и Promise.

В итоге мы получим следующий результат:

foo
foo timeout before
foo timeout after
foo timeout                 // after ~1s
inside then 1: foo resolved // after ~1s
bar
bar timeout before
bar timeout after
inside then 2: undefined    // after ~1s
bar timeout                 // after ~3s

Заключение

Наконец, мы можем сравнить результаты в каждом случае и сказать, какие из них эквивалентны:

  • 1-й и 2-й идентичны, разница только в стиле
  • 3-й и 4-й в целом ведут себя по-разному, но есть и общая часть - они не ждут, пока bar будет Promise.

Надеюсь, этот небольшой пост помог вам лучше понять, как Promises работают в JavaScript, и еще один вопрос собеседования будет легко пройден.