Как использовать Promise.all()
Дата публикации: 2021-08-09
От автора: говоря простыми словами, промисы — это плейсхолдер для значения, которое будет доступно когда-нибудь позже. Промисы полезны при обработке асинхронных операций.
JavaScript предоставляет вспомогательную функцию Promise.all(promisesArrayOrIterable) для одновременной, параллельной обработки нескольких промисов и получения результатов в одном агрегированном массиве. Посмотрим, как это работает.
Promise.all()
Promise.all() — это встроенный хэлпер, который принимает массив промисов (или, как правило, итерацию).
JavaScript. Быстрый старт
Изучите основы JavaScript на практическом примере по созданию веб-приложения
Затем вы можете извлечь значения промисов, используя синтаксис then-able:
или синтаксис async/await:
Интересная часть заключается в том, как промис, возвращаемый функцией Promise.all(), разрешается или отклоняется. Если все промисы разрешены успешно, allPromise выполняется с массивом, содержащим выполненные значения отдельных промисов. Порядок промисов в массиве имеет значение — вы получите значения именно в этом порядке.
Но если хотя бы один промис отклоняется, то allPromise отклоняется сразу (не дожидаясь разрешения других промисов).
Давайте посмотрим на нескольких примерах, как использовать Promise.all() для одновременного выполнения нескольких асинхронных операций.
Пример: все промисы выполнены.
Чтобы изучить, как работает Promise.all(), я собираюсь использовать 2 функции — resolveTimeout(value, delay) и rejectTimeout(reason, delay).
resolveTimeout(value, delay) возвращает промис, который выполняется по прошествии времени delay.
С другой стороны, rejectTimeout(reason, delay) возвращает промис, который отклоняется по причине reason (обычно с ошибкой) по прошествии времени delay.
JavaScript. Быстрый старт
Изучите основы JavaScript на практическом примере по созданию веб-приложения
Например, давайте получим доступ к спискам овощей и фруктов, доступных в местном продуктовом магазине. Доступ к каждому списку — асинхронная операция:
const allPromise = Promise.all([. ]) возвращает новый allPromise.
Затем оператор const lists = await allPromise ожидает 1 секунду, пока не будет выполнен allPromise, содержащий значения выполнения первого и второго промисов.
Наконец, lists содержит агрегированный результат: [[‘potatoes’, ‘tomatoes’], [‘oranges’, ‘apples’]].
Порядок массива промисов напрямую влияет на порядок результатов. Промис из овощей это первый элемент, а промис из фруктов — второй элемент в массиве: Promise.all([vegetablesPromise, fruitsPromise]). Массив результатов содержит значения в том же порядке — первый список овощей и второй список фруктов.
Пример: один промис отклоняется.
А теперь представьте ситуацию, когда в магазине закончились фрукты. В таком случае давайте откажемся от промиса фруктов с ошибкой new Error(‘Out of fruits!’):
Источник
Promise API
В классе Promise есть 5 статических методов. Давайте познакомимся с ними.
Promise.all
Допустим, нам нужно запустить множество промисов параллельно и дождаться, пока все они выполнятся.
Например, параллельно загрузить несколько файлов и обработать результат, когда он готов.
Для этого как раз и пригодится Promise.all .
Метод Promise.all принимает массив промисов (может принимать любой перебираемый объект, но обычно используется массив) и возвращает новый промис.
Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их результатов.
Например, Promise.all , представленный ниже, выполнится спустя 3 секунды, его результатом будет массив [1, 2, 3] :
Обратите внимание, что порядок элементов массива в точности соответствует порядку исходных промисов. Даже если первый промис будет выполняться дольше всех, его результат всё равно будет первым в массиве.
Часто применяемый трюк – пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернёт получившийся массив в Promise.all .
Например, если у нас есть массив ссылок, то мы можем загрузить их вот так:
А вот пример побольше, с получением информации о пользователях GitHub по их логинам из массива (мы могли бы получать массив товаров по их идентификаторам, логика та же):
Если любой из промисов завершится с ошибкой, то промис, возвращённый Promise.all , немедленно завершается с этой ошибкой.
Здесь второй промис завершится с ошибкой через 2 секунды. Это приведёт к немедленной ошибке в Promise.all , так что выполнится .catch : ошибка этого промиса становится ошибкой всего Promise.all .
Если один промис завершается с ошибкой, то весь Promise.all завершается с ней, полностью забывая про остальные промисы в списке. Их результаты игнорируются.
Например, если сделано несколько вызовов fetch , как в примере выше, и один не прошёл, то остальные будут всё ещё выполняться, но Promise.all за ними уже не смотрит. Скорее всего, они так или иначе завершатся, но их результаты будут проигнорированы.
Promise.all ничего не делает для их отмены, так как в промисах вообще нет концепции «отмены». В главе Fetch: прерывание запроса мы рассмотрим AbortController , который помогает с этим, но он не является частью Promise API.
Обычно, Promise.all(. ) принимает перебираемый объект промисов (чаще всего массив). Но если любой из этих объектов не является промисом, он передаётся в итоговый массив «как есть».
Например, здесь результат: [1, 2, 3]
Таким образом, мы можем передавать уже готовые значения, которые не являются промисами, в Promise.all , иногда это бывает удобно.
Promise.allSettled
Promise.all завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для ситуаций «всё или ничего», когда нам нужны все результаты для продолжения:
Метод Promise.allSettled всегда ждёт завершения всех промисов. В массиве результатов будет
Например, мы хотели бы загрузить информацию о множестве пользователей. Даже если в каком-то запросе ошибка, нас всё равно интересуют остальные.
Используем для этого Promise.allSettled :
Массив results в строке (*) будет таким:
То есть, для каждого промиса у нас есть его статус и значение/ошибка.
Полифил
Если браузер не поддерживает Promise.allSettled , для него легко сделать полифил:
В этом коде promises.map берёт аргументы, превращает их в промисы (на всякий случай) и добавляет каждому обработчик .then .
Этот обработчик превращает успешный результат value в
Затем мы можем использовать Promise.allSettled , чтобы получить результаты всех промисов, даже если при выполнении какого-то возникнет ошибка.
Promise.race
Метод очень похож на Promise.all , но ждёт только первый промис, из которого берёт результат (или ошибку).
Например, тут результат будет 1 :
Быстрее всех выполнился первый промис, он и дал результат. После этого остальные промисы игнорируются.
Promise.resolve/reject
Методы Promise.resolve и Promise.reject редко используются в современном коде, так как синтаксис async/await (мы рассмотрим его чуть позже) делает его, в общем-то, не нужным.
Мы рассмотрим их здесь для полноты картины, а также для тех, кто по каким-то причинам не может использовать async/await .
- Promise.resolve(value) создаёт успешно выполненный промис с результатом value .
То же самое, что:
Этот метод используют для совместимости: когда ожидается, что функция возвратит именно промис.
Например, функция loadCached ниже загружает URL и запоминает (кеширует) его содержимое. При будущих вызовах с тем же URL он тут же читает предыдущее содержимое из кеша, но использует Promise.resolve , чтобы сделать из него промис, для того, чтобы возвращаемое значение всегда было промисом:
Мы можем писать loadCached(url).then(…) , потому что функция loadCached всегда возвращает промис. Мы всегда можем использовать .then после loadCached . Это и есть цель использования Promise.resolve в строке (*) .
Promise.reject
- Promise.reject(error) создаёт промис, завершённый с ошибкой error .
То же самое, что:
На практике этот метод почти никогда не используется.
Итого
Мы ознакомились с пятью статическими методами класса Promise :
Источник
Цепочка промисов
Давайте вернёмся к ситуации из главы Введение: колбэки: у нас есть последовательность асинхронных задач, которые должны быть выполнены одна за другой. Например, речь может идти о загрузке скриптов. Как же грамотно реализовать это в коде?
Промисы предоставляют несколько способов решения подобной задачи.
В этой главе мы разберём цепочку промисов.
Она выглядит вот так:
Идея состоит в том, что результат первого промиса передаётся по цепочке обработчиков .then .
Поток выполнения такой:
- Начальный промис успешно выполняется через 1 секунду (*) ,
- Затем вызывается обработчик в .then (**) .
- Возвращаемое им значение передаётся дальше в следующий обработчик .then (***)
- …и так далее.
В итоге результат передаётся по цепочке обработчиков, и мы видим несколько alert подряд, которые выводят: 1 → 2 → 4 .
Всё это работает, потому что вызов promise.then тоже возвращает промис, так что мы можем вызвать на нём следующий .then .
Когда обработчик возвращает какое-то значение, то оно становится результатом выполнения соответствующего промиса и передаётся в следующий .then .
Классическая ошибка новичков: технически возможно добавить много обработчиков .then к единственному промису. Но это не цепочка.
Мы добавили несколько обработчиков к одному промису. Они не передают друг другу результаты своего выполнения, а действуют независимо.
Вот картина происходящего (сравните это с изображением цепочки промисов выше):
Все обработчики .then на одном и том же промисе получают одно и то же значение – результат выполнения того же самого промиса. Таким образом, в коде выше все alert показывают одно и то же: 1 .
На практике весьма редко требуется назначать несколько обработчиков одному промису. А вот цепочка промисов используется куда чаще.
Возвращаем промисы
Обработчик handler , переданный в .then(handler) , может вернуть промис.
В этом случае дальнейшие обработчики ожидают, пока он выполнится, и затем получают его результат.
Здесь первый .then показывает 1 и возвращает новый промис new Promise(…) в строке (*) . Через одну секунду этот промис успешно выполняется, и его результат (аргумент в resolve , то есть result * 2 ) передаётся обработчику в следующем .then . Он находится в строке (**) , показывает 2 и делает то же самое.
Таким образом, как и в предыдущем примере, выводятся 1 → 2 → 4, но сейчас между вызовами alert существует пауза в 1 секунду.
Возвращая промисы, мы можем строить цепочки из асинхронных действий.
Пример: loadScript
Давайте используем эту возможность вместе с промисифицированной функцией loadScript , созданной нами в предыдущей главе, чтобы загружать скрипты по очереди, последовательно:
Этот же код можно переписать немного компактнее, используя стрелочные функции:
Здесь каждый вызов loadScript возвращает промис, и следующий обработчик в .then срабатывает, только когда этот промис завершается. Затем инициируется загрузка следующего скрипта и так далее. Таким образом, скрипты загружаются один за другим.
Мы можем добавить и другие асинхронные действия в цепочку. Обратите внимание, что наш код всё ещё «плоский», он «растёт» вниз, а не вправо. Нет никаких признаков «адской пирамиды вызовов».
Технически мы бы могли добавлять .then напрямую к каждому вызову loadScript , вот так:
Этот код делает то же самое: последовательно загружает 3 скрипта. Но он «растёт вправо», так что возникает такая же проблема, как и с колбэками.
Разработчики, которые не так давно начали использовать промисы, иногда не знают про цепочки и пишут код именно так, как показано выше. В целом, использование цепочек промисов предпочтительнее.
Иногда всё же приемлемо добавлять .then напрямую, чтобы вложенная в него функция имела доступ к внешней области видимости. В примере выше самая глубоко вложенная функция обратного вызова имеет доступ ко всем переменным script1 , script2 , script3 . Но это скорее исключение, чем правило.
Если быть более точными, обработчик может возвращать не именно промис, а любой объект, содержащий метод .then , такие объекты называют «thenable», и этот объект будет обработан как промис.
Смысл в том, что сторонние библиотеки могут создавать свои собственные совместимые с промисами объекты. Они могут иметь свои наборы методов и при этом быть совместимыми со встроенными промисами, так как реализуют метод .then .
Вот пример такого объекта:
JavaScript проверяет объект, возвращаемый из обработчика .then в строке (*) : если у него имеется метод then , который можно вызвать, то этот метод вызывается, и в него передаются как аргументы встроенные функции resolve и reject , вызов одной из которых потом ожидается. В примере выше происходит вызов resolve(2) через 1 секунду (**) . Затем результат передаётся дальше по цепочке.
Это позволяет добавлять в цепочки промисов пользовательские объекты, не заставляя их наследовать от Promise .
Более сложный пример: fetch
Во фронтенд-разработке промисы часто используются, чтобы делать запросы по сети. Давайте рассмотрим один такой пример.
Мы будем использовать метод fetch, чтобы подгрузить информацию о пользователях с удалённого сервера. Этот метод имеет много опциональных параметров, разобранных в соответствующих разделах, но базовый синтаксис весьма прост:
Этот код запрашивает по сети url и возвращает промис. Промис успешно выполняется и в свою очередь возвращает объект response после того, как удалённый сервер присылает заголовки ответа, но до того, как весь ответ сервера полностью загружен.
Чтобы прочитать полный ответ, надо вызвать метод response.text() : он тоже возвращает промис, который выполняется, когда данные полностью загружены с удалённого сервера, и возвращает эти данные.
Код ниже запрашивает файл user.json и загружает его содержимое с сервера:
Есть также метод response.json() , который читает данные в формате JSON. Он больше подходит для нашего примера, так что давайте использовать его.
Мы также применим стрелочные функции для более компактной записи кода:
Теперь давайте что-нибудь сделаем с полученными данными о пользователе.
Например, мы можем послать запрос на GitHub, чтобы загрузить данные из профиля пользователя и показать его аватар:
Код работает, детали реализации отражены в комментариях. Однако в нём есть одна потенциальная проблема, с которой часто сталкиваются новички.
Посмотрите на строку (*) : как мы можем предпринять какие-то действия после того, как аватар был показан и удалён? Например, мы бы хотели показывать форму редактирования пользователя или что-то ещё. Сейчас это невозможно.
Чтобы сделать наш код расширяемым, нам нужно возвращать ещё один промис, который выполняется после того, как завершается показ аватара.
То есть, обработчик .then в строке (*) будет возвращать new Promise , который перейдёт в состояние «выполнен» только после того, как в setTimeout (**) будет вызвана resolve(githubUser) .
Соответственно, следующий по цепочке .then будет ждать этого.
Как правило, все асинхронные действия должны возвращать промис.
Это позволяет планировать после него какие-то дополнительные действия. Даже если эта возможность не нужна прямо сейчас, она может понадобиться в будущем.
И, наконец, давайте разобьём написанный код на отдельные функции, пригодные для повторного использования:
Итого
Если обработчик в .then (или в catch/finally , без разницы) возвращает промис, последующие элементы цепочки ждут, пока этот промис выполнится. Когда это происходит, результат его выполнения (или ошибка) передаётся дальше.
Источник