JavaScript. Promise API
01.06.2022
Теги: API • Backend • Frontend • JavaScript • Web-разработка • АсинхронныйКод • Теория
Объект Promise используется для отложенных и асинхронных вычислений. Представляет собой обёртку для значения, неизвестного на момент создания промиса. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными. Вместо конечного результата асинхронного метода возвращается обещание получить результат в некоторый момент в будущем.
Promise.all
Допустим, нам нужно запустить множество промисов параллельно и дождаться, пока все они выполнятся. Например, параллельно загрузить несколько файлов и обработать результат, когда он готов. Для этого как раз и пригодится Promise.all
.
let promise = Promise.all(iterable);
Метод Promise.all
принимает массив промисов (может принимать любой перебираемый объект, но обычно используется массив) и возвращает новый промис. Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их результатов.
// Promise.all выполнится спустя 3 секунды c результатом [1,2,3] Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(res => console.log(res));
Обратите внимание, что порядок элементов массива в точности соответствует порядку исходных промисов. Даже если первый промис будет выполняться дольше всех, его результат всё равно будет первым в массиве.
Часто применяемый трюк — пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернуть получившийся массив в Promise.all
.
let urls = [ 'https://api.github.com/users/sindresorhus', 'https://api.github.com/users/donnemartin', 'https://api.github.com/users/kamranahmedse' ]; // Преобразуем каждый URL в промис, возвращённый fetch let requests = urls.map(url => fetch(url)); // Promise.all будет ожидать выполнения всех промисов Promise.all(requests) .then(responses => responses.forEach( response => console.log(`Для URL ${response.url} HTTP-статус ${response.status}`) ));
Для URL https://api.github.com/users/sindresorhus HTTP-статус 200 Для URL https://api.github.com/users/donnemartin HTTP-статус 200 Для URL https://api.github.com/users/kamranahmedse HTTP-статус 200
Ниже пример побольше, с получением информации о пользователях GitHub по их логинам из массива (мы могли бы получать массив товаров по их идентификаторам, логика та же).
let names = ['sindresorhus', 'donnemartin', 'kamranahmedse']; let requests = names.map(name => fetch(`https://api.github.com/users/${name}`)); Promise.all(requests) .then(responses => { // все промисы успешно завершены, но пока у нас только заголовки http-ответов for (let response of responses) { console.log(`Для URL ${response.url} HTTP-статус ${response.status}`); } return responses; }) .then(responses => { // получить тело каждого ответа в json-формате и преобразовать json в объект let users = responses.map(response => response.json()); return Promise.all(users); }) // все json-ответы обработаны, users — массив объектов с результатами http-ответов .then(users => users.forEach(user => console.log(`Имя ${user.name}, блог ${user.blog}`)));
Для URL https://api.github.com/users/sindresorhus HTTP-статус 200 Для URL https://api.github.com/users/donnemartin HTTP-статус 200 Для URL https://api.github.com/users/kamranahmedse HTTP-статус 200 Имя Sindre Sorhus, блог https://sindresorhus.com/apps Имя Donne Martin, блог http://donnemartin.com/ Имя Kamran Ahmed, блог youtube.com/theroadmap
Если любой из промисов завершится с ошибкой, то промис, возвращённый Promise.all
, немедленно завершается с этой ошибкой:
Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error('Ошибка!')), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Ошибка!
Здесь второй промис завершится с ошибкой через 2 секунды. Это приведёт к немедленной ошибке в Promise.all
, так что выполнится catch()
— ошибка этого промиса становится ошибкой всего Promise.all
.
Обычно Promise.all
принимает перебираемый объект промисов (чаще всего массив). Но если любой из этих объектов не является промисом, он передаётся в итоговый массив «как есть».
Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), 2 ]).then(res => console.log(res)); // [1, 2]
Promise.allSettled
let promise = Promise.allSettled(iterable);
Promise.all
завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для ситуаций «всё или ничего», когда нам нужны все результаты для продолжения.
Promise.all([ fetch('/template.html'), fetch('/style.css'), fetch('/data.json') ]).then(render); // методу render нужны результаты всех fetch
Метод Promise.allSettled
всегда ждёт завершения всех промисов. В массиве результатов будут следующие элементы:
{status:"fulfilled", value:результат}
— для успешных завершений{status:"rejected", reason:ошибка}
— для ошибок
Например, мы хотели бы загрузить информацию о множестве пользователей. Даже если в каком-то запросе ошибка, нас всё равно интересуют остальные.
let urls = [ 'https://api.github.com/users/sindresorhus', 'https://api.github.com/users/donnemartin', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { results.forEach((result, num) => { if (result.status == 'fulfilled') { console.log(`Для URL ${urls[num]} HTTP-статус ${result.value.status}`); } if (result.status == 'rejected') { console .log(`Для URL ${urls[num]} ошибка ${result.reason}`); } }); });
Для URL https://api.github.com/users/sindresorhus HTTP-статус 200 Для URL https://api.github.com/users/donnemartin HTTP-статус 200 Для URL https://no-such-url ошибка TypeError: Failed to fetch
Если браузер не поддерживает Promise.allSettled
, для него легко сделать полифил:
if (!Promise.allSettled) { Promise.allSettled = function(promises) { return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ status: 'fulfilled', value: value }), error => ({ status: 'rejected', reason: error })))); }; }
Promise.race
let promise = Promise.race(iterable);
Метод очень похож на Promise.all
, но ждёт только первый выполненный промис, из которого берёт результат (или ошибку). Например, в коде ниже результат будет единица.
Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error('Ошибка!')), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(result => alert(result)); // 1
Быстрее всех выполнился первый промис, он и дал результат. После этого остальные промисы игнорируются.
Promise.resolve/reject
Методы Promise.resolve
и Promise.reject
редко используются в современном коде, так как синтаксис async/await
делает их не нужными.
Promise.resolve
Метод Promise.resolve(value)
создаёт успешно выполненный промис с результатом value
. То же самое, что:
let promise = new Promise(resolve => resolve(value));
Например, функция loadCached
ниже загружает URL и запоминает (кеширует) его содержимое. При будущих вызовах с тем же URL он тут же читает предыдущее содержимое из кеша, но использует Promise.resolve
, чтобы сделать из него промис, для того, чтобы возвращаемое значение всегда было промисом.
let cache = new Map(); function loadCached(url) { if (cache.has(url)) { return Promise.resolve(cache.get(url)); } return fetch(url) .then(response => response.text()) .then(text => { cache.set(url,text); return text; }); }
Мы можем писать loadCached(url).then(…)
, потому что функция loadCached
всегда возвращает промис — в этом и есть цель использования Promise.resolve
в этом коде.
Promise.reject
Метод Promise.reject(error)
создаёт промис, завершённый с ошибкой error
. То же самое, что:
let promise = new Promise((resolve, reject) => reject(error));
На практике этот метод почти никогда не используется.
Поиск: API • Backend • Frontend • JavaScript • Web-разработка • Асинхронный код • Теория • Promise