Главная Категории Контакты Поиск

Что такое JavaScript Promises

Promise - как один из способов работы с асинхронным кодом в JavaScript без написания слишком большого количества обратных вызовов.

JavaScript·02.05.2019·читать 6 мин 🤓·Автор: Alexey Myzgin

Введение в Promises

Промисы часто используются, когда мы запрашиваем JSON API и выполняем AJAX запросы.

Промисы - это то, что произойдет между настоящим моментом и концом времени; или другими словами это то, что произойдет в будущем, но, вероятно, не сразу. Чтобы понять это, нам нужно помнить, что JavaScript почти полностью асинхронный.

Промисы - это один из способов работы с асинхронным кодом в JavaScript без написания слишком большого количества обратных вызовов.

Хоть они и существовали годами, со временем были стандартизированы и представлены в ES2015, а теперь заменены в ES2017 асинхронными функциями.

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

Вкратце о том, как работают промисы

Как только промис был вызван, он запускается в состоянии ожидания (pending state). Это означает, что функция, которая вызвала промис, продолжает выполнение, ожидая, пока промис выполнит свою собственную обработку и предоставит функции вызова некоторую обратную связь.

На этом этапе функция вызова ожидает, пока промис не вернется в resolved state (выполнено успешно) или в rejected state (выполнено с ошибкой), и в то же время она продолжает свое выполнение, пока выполняется промис.

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

Promise API предоставляет конструктор Promise, который мы инициализируем с помощью new Promise() и он принимает одну функцию которая передает нам resolve и reject. Идея заключается в том, что промис либо будет resolve (выполнен успешно) - завершен и передаст нам данные. Или он может сам себя reject (отклонить), потому что, возможно, произошла ошибка, или данные были искажены, или по какой-либо другой причине, которая приведет к ошибке. Мы вызываем resolve или reject (когда готовы завершить промис) и передаем в них данные для этого промиса.

Например:

let done = true;

const p = new Promise((resolve, reject) => {
  if (done) {
    resolve("выполнено успешно");
  } else {
    reject("выполнено с ошибкой");
  }
});

В этом примере промис проверяет глобальную константу done, и если она равна true - мы возвращаем resolve («выполнено успешно»). В противном случае возвращаем rejected («выполнено с ошибкой»).

С помощью resolve и reject мы можем передать значение. В приведенном выше примере мы просто возвращаем строку, но это может быть и объект.

Выполнение промиса

const p = new Promise((resolve, reject) => {
  resolve("Alex is cool");
});

p.then(data => {
  console.log(data); // Alex is cool
});

При запуске p мы выполняем промис и ожидаем успешного выполнения, используя обратный вызов then(). Так как мы создали промис, а затем сразу же выполнили resolve - передав Alex is cool, мы сразу увидем результат в консоле.

Если нужно сделать resolve через некоторое время

Если мы хотим выполнить некоторую обработку в фоновом режиме или сделать AJAX-запрос, а затем, когда данные вернутся, сделать resolve. По сути, всё сводится к тому, что «я не хочу останавливать выполнение JavaScript, я просто хочу начать запрос, а затем, когда он вернется, разберусь с этим результатом».

Давай посмотрим, что произойдет, когда мы установим здесь тайм-аут в 1 секунду.

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Alex is cool");
  }, 1000);
});

p.then(data => {
  console.log(data); // Alex is cool - через 1 секунду
});

Через секунду в консоле появится Alex is cool. Точно так же мы можем вызвать reject:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Err Alex is not cool");
  }, 1000);
});

p.then(data => {
  console.log(data);
});

В консоле увидим Uncaught (in promise) Err Alex is not cool. Err Alex is not cool - это настоящая ошибка. Почему Uncaught (in promise) (не перехвачено промисом)? Потому что мы не словили и не обработали ошибку в промисе. Для этого нам нужно добавить в нашу цепочку catch(), передать ошибку и вывести ее через console.error.

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Err Alex is not cool");
  }, 1000);
});

p.then(data => {
  console.log(data);
}).catch(err => {
  console.error(err);
});

Теперь в консоле браузера через 1 секунду будет только реальная ошибка, которую мы передали. В идеале, для более детальной информации, где произошла ошибка, мы должны передать объект Error, а не просто строку. Поэтому мы обернем строку в объект Error, а затем, когда ошибка выведется в консоль, мы увидим, что у нас есть информация о том, где она произошла.

reject(Error("Err Alex is not cool"));

Цепочка промисов

Промис можно вернуть другому промису и таким образом создать цепочку промисов.

Отличным примером цепочки промисов является Fetch API - слой поверх XMLHttpRequest API. Его мы можем использовать для получения данных, создав цепочку промисов. Они будут выполнятся, когда мы получим данные.

Fetch API - это механизм, основанный на промисах, а вызов fetch() - он эквивалентен определению нашего собственного промиса с использованием new Promise().

В этом примере мы вызываем fetch(), чтобы получить данные о моем профиле на github, и создаем цепочку промисов.

const status = response => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response);
  }
  return Promise.reject(Error(response.statusText));
};

const json = response => response.json();

fetch("https://api.github.com/users/oleksiimyzgin")
  .then(status)
  .then(json)
  .then(data => {
    console.log("Request succeeded with JSON response", data);
  })
  .catch(error => {
    console.error("Request failed", error);
  });

Запуск fetch() возвращает response, который имеет много свойств, из которых мы используем:

  • status - числовое значение, предоставляющее код состояния HTTP;
  • statusText - сообщение о состоянии, которое OK, если запрос был выполнен успешно.

У response также есть метод json(), который возвращает промис. Выполнив его он содержит нужный нам контент тела, обработанный и преобразованный в JSON.

Таким образом, учитывая эти промисы, вот что происходит: первый промис в цепочке - это определенная нами функция, называемая status(), которая проверяет статус ответа и, если ответ успешный (между 200 и 299), то выполнится resolve, в противном случае reject.

Если выполняется reject, то цепочка промисов пропустит все перечисленные then() и перейдет непосредственно к первому catch() внизу, в котором будет записан текст Request failed (Ошибка запроса) вместе с сообщением об ошибке.

Если операция прошла успешно то выполняется resolve, затем вызывается функция json(), которую мы определили. Поскольку предыдущий промис, в случае успеха, вернул объект response, мы получаем его в качестве входных данных для второго промиса.

В этом случае мы возвращаем данные, обработанные JSON, поэтому третий промис получает JSON напрямую:

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})

это мы и выводим в консоль.

Обработка ошибок

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

new Promise((resolve, reject) => {
  throw new Error("New Error");
})
  .then(res => console.log(res))
  .catch(err => {
    console.error(err); // Error: New Error
  });

// или

new Promise((resolve, reject) => {
  reject(Error("New Error"));
})
  .then(res => console.log(res))
  .catch(err => {
    console.error(err); // Error: New Error
  });

Каскадные ошибки

Если внутри catch() мы вызываем ошибку, то можно добавить второй catch() для её обработки и так далее.

new Promise((resolve, reject) => {
  throw new Error("Error 1");
})
  .catch(err => {
    throw new Error("Error 2");
  })
  .catch(err => {
    console.error(err); // Error: Error 2
  });

Оркестровые промисы

Promise.all()

Если нам нужно синхронизировать различные промисы, то Promise.all() поможет определить список промисов и выполнить что-то только тогда, когда все они будут выполнены успешно.

const weather = new Promise(resolve => {
  setTimeout(() => {
    resolve({ temp: 25, condition: "Солнечно" });
  }, 2000);
});

const person = new Promise(resolve => {
  setTimeout(() => {
    resolve({ name: "Alex", dev: "Frontend" });
  }, 500);
});

Promise.all([weather, person])
  .then(res => {
    console.log("Array of results", res);
  })
  .catch(err => {
    console.error(err);
  });

Синтаксис ES2015 деструктуризации позволяет сделать так:

Promise.all([weather, person]).then(([weatherInfo, personInfo]) => {
  console.log("Results", weatherInfo, personInfo);
});

В данном примере мы получим ответ только после 2 секунд, потому что мы ждем, пока каждый промис будет выполнен успешно, прежде чем запустим then. Иными словами, самый медленный response будет решать, через сколько вернутся все промисы.

Это был пример с setTimeouts. Сейчас давай разберем с реальными данными. Нам нужно 2 API, с которых мы получим данные. Берём 2 учетные записи с github.

Так как мы получаем в качестве ответа поток данных, мы должны его преобразовать в читаемый json. Ранее мы делали это с одним ответом через response.json(). Как это сделать если у нас два response? Мы перебираем response с помощью map, который вернет новый массив и на каждом res вызываем второй промис json().

const data1 = fetch("https://api.github.com/users/oleksiimyzgin");
const data2 = fetch("https://api.github.com/users/leoyats");

Promise.all([data1, data2])
  .then(response => {
    return Promise.all(response.map(res => res.json()));
  })
  .then(response => {
    console.log(response);
  });

Почему мы должны вызывать res.json?

Причина в том, что существует много разных типов данных, которые могут вернуться. В MDN документации написано что body может вернуться в виде arrayBuffer, blob, json, text или formData. Но не стоит предполагать, что твой API или AJAX запросы всегда будут json, так как это могут быть данные любого типа, которые там есть.

Promise.race()

Promise.race() возвращает resolve или reject промис, в зависимости от того, с каким результатом завершится первый из переданных ему промисов: со значением или с ошибкой. В данном примере выполнится только самый быстрый промис.

const promiseOne = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Return after 2 seconds");
  }, 2000);
});
const promiseTwo = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Return after 1 second");
  }, 1000);
});

Promise.race([promiseOne, promiseTwo]).then(result => {
  console.log(result); // Return after 1 second
});

Распространенные ошибки

Uncaught TypeError: undefined is not a promise

Если ты получил Uncaught TypeError: undefined is not a promise в консоли - убедись, что ты используешь new Promise(), вместо просто Promise().

Website, name & logo
Copyright © 2022. Alex Myzgin