Прочитав это руководство, ты научишься самостоятельно создавать промисы в JavaScript, работать с цепочками JS промисов (promise chains), функциями Promise.all и Promise.race.

Создание промисов в JavaScript

В JS, промис (и не только) можно создать с помощью ключевого слова new

const promise = new Promise(executor);

Параметр executor - это функция у которой должно быть два параметра (тоже функции):

  • resolve - используется когда все прошло хорошо и нужно вернуть результат
  • reject - используется если возникла ошибка

Функция executor вызывается автоматически, а resolve или reject внутри нее нам нужно вызвать самостоятельно.

Напишем функцию coinflip, которая имитирует бросок монетки. Принимает ставку bet и в половине случае завершается ошибкой, а в половине случаев “думает” 2 секунды и возвращает удвоенную ставку.

const coinflip = (bet) => new Promise((resolve, reject) => {
  const hasWon = Math.random() > 0.5 ? true : false;
  if (hasWon) {
    setTimeout(() => {
      resolve(bet * 2);
    }, 2000);
  } else {
    reject(new Error("Ты проиграл...")); // то же, что и -> throw new Error("Ты проиграл...");
  }
});

В функцию resolve мы передаем значение, которое станет доступно после того, как промис станет fulfilled.

А в reject - ошибку. Технически мы можем не вызывать reject, а просто использовать throw. Разницы никакой не будет.

Давай используем функцию coinflip.

coinflip(10)
  .then(result => {
    console.log(`ПОЗДРАВЛЯЮ! ТЫ ВЫИГРАЛ ${result}!`);
  })
  .catch(e => {
    console.log(e.message);
  })

Как и раньше, если все пройдет хорошо, то результат мы получим внутри then. А ошибки обработаем внутри catch.

JS цепочки промисов

Часто возникают ситуации, когда одно асинхронное действие должно выполниться после другого асинхронного действия.

Например, мы можем попровать поставить еще раз если удалось выиграть coinflip. И потом еще.

Для этого, в JavaScript, можно создавать цепочки промисов. В общем виде они выглядят так:

promise
  .then(...)
  .then(...)
  .then(...)
  .catch(...)

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

А вот обработчика ошибок в .catch нам будет достаточно одного, в самом конце цепочки. Заодно добавим небольшой рефакторинг, чтобы избежать дублирования кода. Смотри:

const betAgain = (result) => {
  console.log(`ПОЗДРАВЛЯЮ! ТЫ ВЫИГРАЛ ${result}!`);
  console.log(`ДАВАЙ СЫГРАЕМ ЕЩЕ!`);
  return coinflip(result);
};

const handleRejection = (e) => {
  console.log(e.message);
};

coinflip(10)
  .then(betAgain)
  .then(betAgain)
  .then(betAgain)
  .then(result => {
    console.log(`УРА! МЫ СДЕЛАЛИ ЭТО! ПОРА ЗАБРАТЬ ${result} ДОМОЙ!`);
  })
  .catch(handleRejection);

Функция betAgain принимает число, выводит на экран поздравления и делает ставку снова. Потом мы добавляем столько блоков .then сколько нам нужно для выполнения задачи.

На самом деле, betAgain нам была нужна только для вывода сообщений внутри. Если бы нас просто интересовало увеличение ставки, то мы бы могли просто передавать в .then функцию coinflip. Вот так:

coinflip(10)
  .then(coinflip)
  .then(coinflip)
  .then(coinflip)
  .then(result => {
    console.log(`УРА! МЫ СДЕЛАЛИ ЭТО! ПОРА ЗАБРАТЬ ${result} ДОМОЙ!`);
  })
  .catch(handleRejection);

Promise.all, ожидание всех промисов

Вернемся из нашего виртуального казино в реальный мир.

Представим, что у нас есть функция getUserData, которая возвращает имя пользователя, его id и список id его друзей. Примерно такой объект:

{
  id: 125,
  name: 'Jack Jones',
  friends: [1, 23, 87, 120]
}

Получаем мы его, конечно, не сразу, а после того как промис станет fulfilled.

И нам поставили задачу — вывести на экран список всех друзей пользователя, но не просто id, а все их данные.

Работать с одним промисом мы уже умеем, начнем с вывода списка id друзей на экран:

getUserData(userId).then(console.log);

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

getUserData(userId)
  .then(userData => {
    return userData.friends.map(getUserData);
  })
  .then(console.log)
  .catch(e => console.log(e.message));

Неплохо. Но на экране мы увидим [ Promise { <pending> }, Promise { <pending> } ] вместо полной информации о друзьях.

К сожалению, у нас не получится добавить еще один then или map, потому что массив у нас уже есть, а промисы внутри него еще в состоянии pending.

Для решения этой проблемы нам будет нужна функция Promise.all(array). Она принимает массив промисов и возвращает один промис.

Этот промис станет fulfilled когда все промисы из array станут fulfilled. А если хотя бы один из них завершится с ошибкой, то статус rejected получит и весь Promise.all.

getUserData(userId)
  .then(userData => {
    return Promise.all(userData.friends.map(getUserData));
  })
  .then(console.log)
  .catch(e => console.log(e.message));

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

Promise.race, ожидание самого быстрого промиса

Если нам нужен результат только одного промиса из множества, то мы можем использовать функцию Promise.race(arr).

Как и Promise.all, она принимает массив промисов и возвращает один промис. Но нельзя предсказать заранее, чему будет равно значение возвращенное из промиса после того, как он перейдет в состояние fulfilled.

С помощью Promise.race мы можем получить результат того промиса, который выполнится быстрее всех в массиве.

const fastPromise = new Promise((resolve, reject) => {
  setTimeout(() => resolve(`fast`), 100);
});

const slowPromise = new Promise((resolve, reject) => {
  setTimeout(() => resolve(`slow`), 200);
});

const arr = [fastPromise, slowPromise];

Promise.race(arr).then(console.log);  // fast

На экран будет выведено сообщение fast через 100 миллисекунд и мы не будем ждать выполнения второго промиса.