JavaScript. Promise API

01.06.2022

Теги: APIBackendFrontendJavaScriptWeb-разработкаАсинхронныйКодТеория

Объект 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

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.