Что такое promise js
Перейти к содержимому

Что такое promise js

  • автор:

Разбираемся с промисами в JavaScript

Доброго времени суток, Хабр! Представляю вашему вниманию перевод статьи «Understanding Promises in JavaScript» автора Sukhjinder Arora.

От автора перевода: Так же, как и сам автор, я надеюсь, что статья оказалась для вас полезной. Пожалуйста, если она и вправду помогла вам узнать для себя что-то новое, то не поленитесь зайти на оригинал статьи и поблагодарить автора! Буду рад вашему фидбеку!

JavaScript — это однопоточный язык программирования, это означает, что за раз может быть выполнено что-то одно. До ES6 мы использовали обратные вызовы, чтобы управлять асинхронными задачами, такими как сетевой запрос.

Используя промисы, мы можем избегать “ад обратных вызовов” и сделать наш код чище, более читабельным и более простым для понимания.

Предположим, что мы хотим асинхронно получить некоторые данные с сервера, используя обратные вызовы мы сделали бы что-то вроде этого:

getData(function(x)< console.log(x); getMoreData(x, function(y)< console.log(y); getSomeMoreData(y, function(z)< console.log(z); >); >); >);

Здесь я запрашиваю некоторые данные с сервера при помощи функции getData(), которая получает данные внутри функции обратного вызова. Внутри функции обратного вызова я запрашиваю дополнительные данные при помощи вызова функции getMoreData(), передавая предыдущие данные как аргумент и так далее.

Это то, что мы называем “адом обратных вызовов”, где каждый обратный вызов вложен внутрь другого, и каждый внутренний обратный вызов зависит от его родителя.

Мы можем переписать приведенный выше фрагмент используя промисы:

getData() .then((x) => < console.log(x); return getMoreData(x); >) .then((y) => < console.log(y); return getSomeMoreData(y); >) .then((z) => < console.log(z); >);

Вы можете видеть, что стало более читабельно, чем в случае первого примера с обратными вызовами.

Что такое Промисы?

Промис(Обещание) — это объект который содержит будущее значение асинхронной операции. Например, если вы запрашиваете некоторые данные с сервера, промис обещает нам получить эти данные, которые мы сможем использовать в будущем.

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

Состояния промисов

Промис в JavaScript, как и обещание в реальной жизни, имеет 3 состояния. Это может быть 1) нерешенный(в ожидании), 2) решенный/resolved (выполненный) или 3) отклоненный/rejected.

Нерешенный или Ожидающий — Промис ожидает, если результат не готов. То есть, ожидает завершение чего-либо(например, завершения асинхронной операции).
Решенный или Выполненный — Промис решен, если результат доступен. То есть, что-то завершило свое выполнение(например, асинхронная операция) и все прошло хорошо.
Отклоненный — Промиc отклонен, если произошла ошибка в процессе выполнения.

Теперь мы знаем, что такое Промис и его терминологию, давайте вернемся назад к практической части промисов.

Создаем Промис

В большинстве случаев вы будете просто использовать промисы, а не создавать их, но все же важно знать как они создаются.

const promise = new Promise((resolve, reject) => < . >);

Мы создали новый промис, используя конструктор Промисов, он принимает один аргумент, обратный вызов, также известный как исполнительная функция, которая принимает 2 обратных вызова, resolve и reject.

Исполнительная функция выполняется сразу же после создания промиса. Промис становится выполненным при помощи вызова resolve(), а отклоненным при помощи reject(). Например:

const promise = new Promise((resolve, reject) => < if(allWentWell) < resolve('Все прошло отлично!'); >else < reject('Что-то пошло не так'); >>);

resolve() и reject() принимают один аргумент, который может быть строкой, числом, логическим выражением, массивом или объектом.

Давайте взглянем на другой пример, чтобы полностью понять как создаются промисы.

const promise = new Promise((resolve, reject) => < const randomNumber = Math.random(); setTimeout(() => < if(randomNumber < .6) < resolve('Все прошло отлично!'); >else < reject('Что-то пошло не так'); >>, 2000); >);

Здесь я создал новый промис используя конструктор Промисов. Промис выполняется или отклоняется через 2 секунды после его создания. Промис выполняется, если randomNumber меньше, чем .6 и отклоняется в остальных случаях.

Когда промис был создан, он будет в состоянии ожидания и его значение будет undefined.

После 2 секунд таймер заканчивается, промис случайным образом либо выполняется, либо отклоняется, и его значением будет то, которое передано в функцию resolve или reject. Ниже пример двух случаев:

Примечание: Промис может быть выполнен или отклонен только один раз. Дальнейшие вызовы resolve() или reject() никак не повлияют на состояние промиса. Пример:

const promise = new Promise((resolve, reject) => < resolve('Promise resolved'); // Промис выполнен reject('Promise rejected'); // Промис уже не может быть отклонен >);

Так как resolve() была вызвана первой, то промис теперь получается статус “выполненный”. Последующий вызов reject() никак не повлияет на состояние промиса.

Использование Промиса

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

Например, запрос данных из API при помощи fetch, которая возвращает промис.

.then() синтаксис: promise.then(successCallback, failureCallback)

successCallback вызывается, если промис был успешно выполнен. Принимает один аргумент, который является значением переданным в resolve().

failureCallback вызывается, если промис был отклонен. Принимает один аргумент, который является значением преданным в reject().

const promise = new Promise((resolve, reject) => < const randomNumber = Math.random(); if(randomNumber < .7) < resolve('Все прошло отлично!'); >else < reject(new Error('Что-то пошло не так')); >>); promise.then((data) => < console.log(data); // вывести 'Все прошло отлично!' >, (error) => < console.log(error); // вывести ошибку >);

Если промис был выполнен, то вызывается successCallback со значением, переданным в resolve(). И если промис был отклонен, то вызывается failureCallback со значением, переданным в reject().

.catch() синтаксис: promise.catch(failureCallback)

Мы используем catch() для обработки ошибок. Это более читабельно, нежели обработка ошибок внутри failureCallback внутри обратного вызова метода then().

const promise = new Promise((resolve, reject) => < reject(new Error('Что-то пошло не так')); >); promise .then((data) => < console.log(data); >) .catch((error) => < console.log(error); // вывести ошибку >);

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

Методы then() и catch() также могут возвращать новый промис, который может быть обработан цепочкой других then() в конце предыдущего метода then().

Мы используем цепочку промисов, когда хотим выполнить последовательность промисов.

const promise1 = new Promise((resolve, reject) => < resolve('Promise1 выполнен'); >); const promise2 = new Promise((resolve, reject) => < resolve('Promise2 выполнен'); >); const promise3 = new Promise((resolve, reject) => < reject('Promise3 отклонен'); >); promise1 .then((data) => < console.log(data); // Promise1 выполнен return promise2; >) .then((data) => < console.log(data); // Promise2 выполнен return promise3; >) .then((data) => < console.log(data); >) .catch((error) => < console.log(error); // Promise3 отклонен >);
И так, что тут происходит?

Когда promise1 выполнен, вызывается метод then(), который возвращает promise2.
Далее, когда выполнен promise2, снова вызывается then() и возвращает promise3.

Так как promise3 отклонен, вместо следующего then(), вызывается catch(), который и обрабатывает отклонение promise3.

Примечание: Как правило достаточно одного метода catch() для обработки отклонения любого из промисов в цепочке, если этот метод находится в конце неё.

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

Достаточно много новичков делают ошибку, вкладывая одни промисы внутрь других. Например:

const promise1 = new Promise((resolve, reject) => < resolve('Promise1 выполнен'); >); const promise2 = new Promise((resolve, reject) => < resolve('Promise2 выполнен'); >); const promise3 = new Promise((resolve, reject) => < reject('Promise3 отклонен'); >); promise1.then((data) => < console.log(data); // Promise1 выполнен promise2.then((data) => < console.log(data); // Promise2 выполнен promise3.then((data) =>< console.log(data); >).catch((error) => < console.log(error); // Promise3 отклонен >); >).catch((error) => < console.log(error); >) >).catch((error) => < console.log(error); >);

Несмотря на то, что это будет работать нормально, это считается плохим стилем и делает код менее читабельным. Если у вас есть последовательность промисов для выполнения, будет лучше ставить их один за другим, нежели вкладывать один внутрь другого.

Promise.all( )

Этот метод берет массив промисов и возвращает новый промис, который будет выполненным, когда все промисы внутри массива выполнены или отклонен, как только встречается промис, который отклоняется. Например:

const promise1 = new Promise((resolve, reject) => < setTimeout(() =>< resolve('Promise1 выполнен'); >, 2000); >); const promise2 = new Promise((resolve, reject) => < setTimeout(() =>< resolve('Promise2 выполнен'); >, 1500); >); Promise.all([promise1, promise2]) .then((data) => console.log(data[0], data[1])) .catch((error) => console.log(error));

Здесь аргументом внутри then() выступает массив, который содержит значения промисов в том же порядке, в котором они передавались в Promise.all().(Только в том случае, если все промисы выполняются)

Промис отклоняется с причиной отклонения первого промиса в переданном массиве. Например:

const promise1 = new Promise((resolve, reject) => < setTimeout(() =>< resolve('Promise1 выполнен'); >, 2000); >); const promise2 = new Promise((resolve, reject) => < setTimeout(() =>< reject('Promise2 отклонен'); >, 1500); >); Promise.all([promise1, promise2]) .then((data) => console.log(data[0], data[1])) .catch((error) => console.log(error)); // Promise2 отклонен

Здесь у нас есть два промиса, где один выполняется через 2 секунды, а другой отклоняется через 1.5 секунды. Как только второй промис отклоняется, возвращенный от Promise.all() промис отклоняется не дожидаясь выполнения первого.

Этот метод может быть полезен, когда у вас есть более одного промиса и вы хотите знать, когда все промисы выполнены. Например, если вы запрашиваете данные из стороннего API и вы хотите что-то сделать с этими данными только тогда, когда все запросы проходят успешно.

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

Promise.race( )

Этот метод принимает массив промисов и возвращает один новый промис, который будет выполненным, как только встретится выполненный промис в массиве или же отклоняется, если отклоненный промис встречается раньше. Например:

const promise1 = new Promise((resolve, reject) => < setTimeout(() =>< resolve('Promise1 выполнен'); >, 1000); >); const promise2 = new Promise((resolve, reject) => < setTimeout(() =>< reject('Promise2 отклонен'); >, 1500); >); Promise.race([promise1, promise2]) .then((data) => console.log(data)) // Promise1 выполнен .catch((error) => console.log(error));

Тут мы имеем два промиса, где один выполняется через 1 секунду, а другой отклоняется через 1.5 секунды. Как только первый промис выполнен, возвращенный из Promise.race() промис будет иметь статус выполненного не дожидаясь статуса второго промиса.

Здесь data, которая передается в then() является значением первого, выполненного, промиса.

По итогу, Promise.race() дожидается первого промиса и берет его статус как статус возвращаемого промиса.

Комментарий автора перевода: Отсюда собственно и название. Race — гонка

Заключение

Мы узнали, что такое промисы и с чем их едят в JavaScript. Промисы состоят из двух частей 1) Создание промиса и 2) Использование промиса. Большую часть времени вы будете пользоваться промисами, нежели создавать их, но важно знать как они создаются.

Вот и все, надеюсь эта статья оказалась для вас полезной!

  • javascript
  • перевод
  • веб-разработка
  • разработка веб-сайтов
  • промисы

Промисы

Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле.

Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также получат уведомление об этом.

Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты больше не беспокоятся, что пропустят новый сингл.

Это аналогия из реальной жизни для ситуаций, с которыми мы часто сталкиваемся в программировании:

  1. Есть «создающий» код, который делает что-то, что занимает время. Например, загружает данные по сети. В нашей аналогии это – «певец».
  2. Есть «потребляющий» код, который хочет получить результат «создающего» кода, когда он будет готов. Он может быть необходим более чем одной функции. Это – «фанаты».
  3. Promise (по англ. promise , будем называть такой объект «промис») – это специальный объект в JavaScript, который связывает «создающий» и «потребляющий» коды вместе. В терминах нашей аналогии – это «список для подписки». «Создающий» код может выполняться сколько потребуется, чтобы получить результат, а промис делает результат доступным для кода, который подписан на него, когда результат готов.

Аналогия не совсем точна, потому что объект Promise в JavaScript гораздо сложнее простого списка подписок: он обладает дополнительными возможностями и ограничениями. Но для начала и такая аналогия хороша.

Синтаксис создания Promise :

let promise = new Promise(function(resolve, reject) < // функция-исполнитель (executor) // "певец" >);

Функция, переданная в конструкцию new Promise , называется исполнитель (executor). Когда Promise создаётся, она запускается автоматически. Она должна содержать «создающий» код, который когда-нибудь создаст результат. В терминах нашей аналогии: исполнитель – это «певец».

Её аргументы resolve и reject – это колбэки, которые предоставляет сам JavaScript. Наш код – только внутри исполнителя.

Когда он получает результат, сейчас или позже – не важно, он должен вызвать один из этих колбэков:

  • resolve(value) — если работа завершилась успешно, с результатом value .
  • reject(error) — если произошла ошибка, error – объект ошибки.

Итак, исполнитель запускается автоматически, он должен выполнить работу, а затем вызвать resolve или reject .

У объекта promise , возвращаемого конструктором new Promise , есть внутренние свойства:

  • state («состояние») — вначале «pending» («ожидание»), потом меняется на «fulfilled» («выполнено успешно») при вызове resolve или на «rejected» («выполнено с ошибкой») при вызове reject .
  • result («результат») — вначале undefined , далее изменяется на value при вызове resolve(value) или на error при вызове reject(error) .

Так что исполнитель по итогу переводит promise в одно из двух состояний:

Позже мы рассмотрим, как «фанаты» узнают об этих изменениях.

Ниже пример конструктора Promise и простого исполнителя с кодом, дающим результат с задержкой (через setTimeout ):

let promise = new Promise(function(resolve, reject) < // эта функция выполнится автоматически, при вызове new Promise // через 1 секунду сигнализировать, что задача выполнена с результатом "done" setTimeout(() =>resolve("done"), 1000); >);

Мы можем наблюдать две вещи, запустив код выше:

  1. Функция-исполнитель запускается сразу же при вызове new Promise .
  2. Исполнитель получает два аргумента: resolve и reject — это функции, встроенные в JavaScript, поэтому нам не нужно их писать. Нам нужно лишь позаботиться, чтобы исполнитель вызвал одну из них по готовности.

Спустя одну секунду «обработки» исполнитель вызовет resolve(«done») , чтобы передать результат:

Это был пример успешно выполненной задачи, в результате мы получили «успешно выполненный» промис.

А теперь пример, в котором исполнитель сообщит, что задача выполнена с ошибкой:

let promise = new Promise(function(resolve, reject) < // спустя одну секунду будет сообщено, что задача выполнена с ошибкой setTimeout(() =>reject(new Error("Whoops!")), 1000); >);

Подведём промежуточные итоги: исполнитель выполняет задачу (что-то, что обычно требует времени), затем вызывает resolve или reject , чтобы изменить состояние соответствующего Promise .

Промис – и успешный, и отклонённый будем называть «завершённым», в отличие от изначального промиса «в ожидании».

Может быть что-то одно: либо результат, либо ошибка

Исполнитель должен вызвать что-то одно: resolve или reject . Состояние промиса может быть изменено только один раз.

Все последующие вызовы resolve и reject будут проигнорированы:

let promise = new Promise(function(resolve, reject) < resolve("done"); reject(new Error("…")); // игнорируется setTimeout(() =>resolve("…")); // игнорируется >);

Идея в том, что задача, выполняемая исполнителем, может иметь только один итог: результат или ошибку.

Также заметим, что функция resolve / reject ожидает только один аргумент (или ни одного). Все дополнительные аргументы будут проигнорированы.

Вызывайте reject с объектом Error

В случае, если что-то пошло не так, мы должны вызвать reject . Это можно сделать с аргументом любого типа (как и resolve ), но рекомендуется использовать объект Error (или унаследованный от него). Почему так? Скоро нам станет понятно.

Вызов resolve / reject сразу

Обычно исполнитель делает что-то асинхронное и после этого вызывает resolve / reject , то есть через какое-то время. Но это не обязательно, resolve или reject могут быть вызваны сразу:

let promise = new Promise(function(resolve, reject) < // задача, не требующая времени resolve(123); // мгновенно выдаст результат: 123 >);

Это может случиться, например, когда мы начали выполнять какую-то задачу, но тут же увидели, что ранее её уже выполняли, и результат закеширован.

Такая ситуация нормальна. Мы сразу получим успешно завершённый Promise .

Свойства state и result – внутренние

Свойства state и result – это внутренние свойства объекта Promise и мы не имеем к ним прямого доступа. Для обработки результата следует использовать методы .then / .catch / .finally , про них речь пойдёт дальше.

Потребители: then, catch

Объект Promise служит связующим звеном между исполнителем («создающим» кодом или «певцом») и функциями-потребителями («фанатами»), которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны) с помощью методов .then и .catch .

then

Наиболее важный и фундаментальный метод – .then .

promise.then( function(result) < /* обработает успешное выполнение */ >, function(error) < /* обработает ошибку */ >);

Первый аргумент метода .then – функция, которая выполняется, когда промис переходит в состояние «выполнен успешно», и получает результат.

Второй аргумент .then – функция, которая выполняется, когда промис переходит в состояние «выполнен с ошибкой», и получает ошибку.

Например, вот реакция на успешно выполненный промис:

let promise = new Promise(function(resolve, reject) < setTimeout(() =>resolve("done!"), 1000); >); // resolve запустит первую функцию, переданную в .then promise.then( result => alert(result), // выведет "done!" через одну секунду error => alert(error) // не будет запущена );

Выполнилась первая функция.

А в случае ошибки в промисе – выполнится вторая:

let promise = new Promise(function(resolve, reject) < setTimeout(() =>reject(new Error("Whoops!")), 1000); >); // reject запустит вторую функцию, переданную в .then promise.then( result => alert(result), // не будет запущена error => alert(error) // выведет "Error: Whoops!" спустя одну секунду );

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

let promise = new Promise(resolve => < setTimeout(() =>resolve("done!"), 1000); >); promise.then(alert); // выведет "done!" спустя одну секунду

catch

Если мы хотели бы только обработать ошибку, то можно использовать null в качестве первого аргумента: .then(null, errorHandlingFunction) . Или можно воспользоваться методом .catch(errorHandlingFunction) , который сделает то же самое:

let promise = new Promise((resolve, reject) => < setTimeout(() =>reject(new Error("Ошибка!")), 1000); >); // .catch(f) это то же самое, что promise.then(null, f) promise.catch(alert); // выведет "Error: Ошибка!" спустя одну секунду

Вызов .catch(f) – это сокращённый, «укороченный» вариант .then(null, f) .

Очистка: finally

По аналогии с блоком finally из обычного try <. >catch <. >, у промисов также есть метод finally .

Вызов .finally(f) похож на .then(f, f) , в том смысле, что f выполнится в любом случае, когда промис завершится: успешно или с ошибкой.

Идея finally состоит в том, чтобы настроить обработчик для выполнения очистки/доведения после завершения предыдущих операций.

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

Думайте об этом как о завершении вечеринки. Независимо от того, была ли вечеринка хорошей или плохой, сколько на ней было друзей, нам все равно нужно (или, по крайней мере, мы должны) сделать уборку после нее.

Код может выглядеть следующим образом:

new Promise((resolve, reject) => < /* сделать что-то, что займёт время, и после вызвать resolve или может reject */ >) // выполнится, когда промис завершится, независимо от того, успешно или нет .finally(() => остановить индикатор загрузки) // таким образом, индикатор загрузки всегда останавливается, прежде чем мы продолжим .then(result => показать результат, err => показать ошибку)

Обратите внимание, что finally(f) – это не совсем псевдоним then(f,f) , как можно было подумать.

Есть важные различия:

  1. Обработчик, вызываемый из finally , не имеет аргументов. В finally мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача – выполнить «общие» завершающие процедуры. Пожалуйста, взгляните на приведенный выше пример: как вы можете видеть, обработчик finally не имеет аргументов, а результат promise обрабатывается в следующем обработчике.
  2. Обработчик finally «пропускает» результат или ошибку дальше, к последующим обработчикам. Например, здесь результат проходит через finally к then :

new Promise((resolve, reject) => < setTimeout(() =>resolve("value"), 2000); >) .finally(() => alert("Промис завершён")) // срабатывает первым .then(result => alert(result)); // 

Как вы можете видеть, значение возвращаемое первым промисом, передается через finally к следующему then . Это очень удобно, потому что finally не предназначен для обработки результата промиса. Как уже было сказано, это место для проведения общей очистки, независимо от того, каков был результат. А здесь ошибка из промиса проходит через finally к catch :

new Promise((resolve, reject) => < throw new Error("error"); >) .finally(() => alert("Промис завершён")) // срабатывает первым .catch(err => alert(err)); // 
  • Обработчик finally не получает результат предыдущего обработчика (у него нет аргументов). Вместо этого этот результат передается следующему подходящему обработчику.
  • Если обработчик finally возвращает что-то, это игнорируется.
  • Когда finally выдает ошибку, выполнение переходит к ближайшему обработчику ошибок.

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

На завершённых промисах обработчики запускаются сразу

Если промис в состоянии ожидания, обработчики в .then/catch/finally будут ждать его.

Иногда может случиться так, что промис уже выполнен, когда мы добавляем к нему обработчик.

В таком случае эти обработчики просто запускаются немедленно:

// при создании промиса он сразу переводится в состояние "успешно завершён" let promise = new Promise(resolve => resolve("готово!")); promise.then(alert); // готово! (выведется сразу)

Пример: loadScript

Теперь рассмотрим несколько практических примеров того, как промисы могут облегчить нам написание асинхронного кода.

У нас есть функция loadScript для загрузки скрипта из предыдущей главы.

Давайте вспомним, как выглядел вариант с колбэками:

function loadScript(src, callback) < let script = document.createElement('script'); script.src = src; script.onload = () =>callback(null, script); script.onerror = () => callback(new Error(`Ошибка загрузки скрипта $`)); document.head.append(script); >

Теперь перепишем её, используя Promise .

Новой функции loadScript более не нужен аргумент callback . Вместо этого она будет создавать и возвращать объект Promise , который перейдет в состояние «успешно завершён», когда загрузка закончится. Внешний код может добавлять обработчики («подписчиков»), используя .then :

function loadScript(src) < return new Promise(function(resolve, reject) < let script = document.createElement('script'); script.src = src; script.onload = () =>resolve(script); script.onerror = () => reject(new Error(`Ошибка загрузки скрипта $`)); document.head.append(script); >); >
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`$ загружен!`), error => alert(`Ошибка: $`) ); promise.then(script => alert('Ещё один обработчик. '));

Сразу заметно несколько преимуществ перед подходом с использованием колбэков:

Промисы Колбэки
Промисы позволяют делать вещи в естественном порядке. Сперва мы запускаем loadScript(script) , и затем ( .then ) мы пишем, что делать с результатом. У нас должна быть функция callback на момент вызова loadScript(script, callback) . Другими словами, нам нужно знать что делать с результатом до того, как вызовется loadScript .
Мы можем вызывать .then у Promise столько раз, сколько захотим. Каждый раз мы добавляем нового «фаната», новую функцию-подписчика в «список подписок». Больше об этом в следующей главе: Цепочка промисов. Колбэк может быть только один.

Таким образом, промисы позволяют улучшить порядок кода и дают нам гибкость. Но это далеко не всё. Мы узнаем ещё много полезного в последующих главах.

Использование промисов

Promise (промис) - это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Так как большинство людей пользуются уже созданными промисами, это руководство начнём с объяснения использования вернувшихся промисов до объяснения принципов создания.

В сущности, промис - это возвращаемый объект, в который вы записываете два колбэка вместо того, чтобы передать их функции.

Например, вместо старомодной функции, которая принимает два колбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:

function doSomethingOldStyle(successCallback, failureCallback)  console.log("Готово."); // Успех в половине случаев. if (Math.random() > 0.5)  successCallback("Успех"); > else  failureCallback("Ошибка"); > > function successCallback(result)  console.log("Успешно завершено с результатом " + result); > function failureCallback(error)  console.log("Завершено с ошибкой " + error); > doSomethingOldStyle(successCallback, failureCallback); 

…современные функции возвращают промис, в который вы записываете ваши колбэки:

function doSomething()  return new Promise((resolve, reject) =>  console.log("Готово."); // Успех в половине случаев. if (Math.random() > 0.5)  resolve("Успех"); > else  reject("Ошибка"); > >); > const promise = doSomething(); promise.then(successCallback, failureCallback); 
doSomething().then(successCallback, failureCallback); 

Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.

Гарантии

В отличие от старомодных переданных колбэков промис даёт некоторые гарантии:

  • Колбэки никогда не будут вызваны до завершения обработки текущего события в событийном цикле JavaScript.
  • Колбэки, добавленные через .then даже после успешного или неудачного завершения асинхронной операции, будут также вызваны.
  • Несколько колбэков может быть добавлено вызовом .then нужное количество раз, и они будут выполняться независимо в порядке добавления.

Но наиболее непосредственная польза от промисов - цепочка вызовов (chaining).

Цепочка вызовов

Общая нужда - выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (promise chain).

Вот в чём магия: функция then возвращает новый промис, отличающийся от первоначального:

let promise = doSomething(); let promise2 = promise.then(successCallback, failureCallback); 
let promise2 = doSomething().then(successCallback, failureCallback); 

Второй промис представляет завершение не только doSomething() , но и функций successCallback или failureCallback , переданных вами, а они тоже могут быть асинхронными функциями, возвращающими промис. В этом случае все колбэки, добавленные к promise2 будут поставлены в очередь за промисом, возвращаемым successCallback или failureCallback .

По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.

Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической "Вавилонской башне" колбэков:

doSomething(function (result)  doSomethingElse( result, function (newResult)  doThirdThing( newResult, function (finalResult)  console.log("Итоговый результат: " + finalResult); >, failureCallback, ); >, failureCallback, ); >, failureCallback); 

В современных функциях мы записываем колбэки в возвращаемые промисы - формируем цепочку промисов:

doSomething() .then(function (result)  return doSomethingElse(result); >) .then(function (newResult)  return doThirdThing(newResult); >) .then(function (finalResult)  console.log("Итоговый результат: " + finalResult); >) .catch(failureCallback); 

Аргументы then необязательны, а catch(failureCallback) - это сокращение для then(null, failureCallback) . Вот как это выражено с помощью стрелочных функций:

doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) =>  console.log(`Итоговый результат: $finalResult>`); >) .catch(failureCallback); 

Важно: Всегда возвращайте промисы в return, иначе колбэки не будут сцеплены и ошибки могут быть не пойманы (стрелочные функции неявно возвращают результат, если скобки <> вокруг тела функции опущены).

Цепочка вызовов после catch

Можно продолжить цепочку вызовов после ошибки, т. е. после catch , что полезно для выполнения новых действий даже после того, как действие вернёт ошибку в цепочке вызовов. Ниже приведён пример:

new Promise((resolve, reject) => < console.log('Начало'); resolve(); >) .then(() => < throw new Error('Где-то произошла ошибка'); console.log('Выведи это'); >) .catch(() => < console.log('Выведи то'); >) .then(() => < console.log('Выведи это, несмотря ни на что'); >);

В результате выведется данный текст:

Начало Выведи то Выведи это, несмотря ни на что

Заметьте, что текст "Выведи это" не вывелся, потому что "Где-то произошла ошибка" привела к отказу

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

Вы могли ранее заметить, что failureCallback повторяется три раза в "pyramid of doom", а в цепочке промисов всего лишь один раз:

doSomething() .then(result => doSomethingElse(result)) .then(newResult => doThirdThing(newResult)) .then(finalResult => console.log(`Итоговый результат: $`)) .catch(failureCallback);

В основном, цепочка промисов останавливает выполнение кода, если где-либо произошла ошибка, и вместо этого ищет далее по цепочке обработчики ошибок. Это очень похоже на то, как работает синхронный код:

try < let result = syncDoSomething(); let newResult = syncDoSomethingElse(result); let finalResult = syncDoThirdThing(newResult); console.log(`Итоговый результат: $`); > catch(error)

Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async / await в ECMAScript 2017:

async function foo() < try < let result = await doSomething(); let newResult = await doSomethingElse(result); let finalResult = await doThirdThing(newResult); console.log(`Итоговый результат: $`); > catch(error) < failureCallback(error); >>

Работа данного кода основана на промисах. Для примера здесь используется функция doSomething() , которая встречалась ранее. Вы можете прочитать больше о синтаксисе здесь

Промисы решают основную проблему пирамид, обработку всех ошибок, даже вызовов исключений и программных ошибок. Это основа для функционального построения асинхронных операций.

Создание промиса вокруг старого колбэка

Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.

В идеале, все асинхронные функции уже должны возвращать промис. Но увы, некоторые APIs до сих пор ожидают успешного или неудачного колбэка переданных по старинке. Типичный пример: setTimeout() функция:

setTimeout(() => saySomething("10 seconds passed"), 10000);

Смешивание старого колбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.

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

const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

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

Композиция

Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклонённые промисы соответственно. Это иногда бывает полезно.

Promise.all() и Promise.race() - два метода запустить асинхронные операции параллельно.

Последовательное выполнение композиции возможно при помощи хитрости JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);

Это также можно сделать, объединив композицию в функцию, в функциональном стиле программирования:

const applyAsync = (acc,val) => acc.then(val); const composeAsync = (. funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync функция примет любое количество функций в качестве аргументов и вернёт новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными, либо синхронными, и они гарантированно выполнятся в правильной последовательности:

const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2); transformData(data);

В ECMAScript 2017, последовательные композиции могут быть выполнены более простым способом с помощью async/await:

for (const f of [func1, func2])

Порядок выполнения

Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызваны синхронно, даже с уже разрешённым промисом:

Promise.resolve().then(() => console.log(2)); console.log(1); // 1, 2

Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait().then(() => console.log(4)); Promise.resolve().then(() => console.log(2)).then(() => console.log(3)); console.log(1); // 1, 2, 3, 4

Вложенность

Простые цепочки promise лучше оставлять без вложений, так как вложенность может быть результатом небрежной структуры. Смотрите распространённые ошибки.

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

doSomethingCritical() .then(result => doSomethingOptional() .then(optionalResult => doSomethingExtraNice(optionalResult)) .catch(e => <>)) // Игнорируется если необязательные параметр не выкинул исключение .then(() => moreCriticalStuff()) .catch(e => console.log("Критическая ошибка: " + e.message));

Обратите внимание, что необязательный шаги здесь выделены отступом.

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

Частые ошибки

В этом разделе собраны частые ошибки, возникающие при создании цепочек промисов. Несколько таких ошибок можно увидеть в следующем примере:

// Плохой пример! Три ошибки! doSomething().then(function(result) < doSomethingElse(result) // Забыл вернуть промис из внутренней цепочки + неуместное влаживание .then(newResult =>doThirdThing(newResult)); >).then(() => doFourthThing()); // Забыл закончить цепочку методом catch

Первая ошибка это неправильно сцепить вещи между собой. Такое происходит когда мы создаём промис но забываем вернуть его. Как следствие, цепочка сломана, но правильнее было бы сказать что теперь у нас есть две независимые цепочки, соревнующиеся за право разрешится первой. Это означает, что doFourthThing() не будет ждать doSomethingElse() или doThirdThing() пока тот закончится, и будет исполнятся параллельно с ними, это, вероятно, не то что хотел разработчик. Отдельные цепочки также имеют отдельную обработку ошибок, что приводит к необработанным ошибкам.

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

Третья ошибка это забыть закончить цепочку ключевым словом catch . Незаконченные цепочки приводят к необработанным отторжениям промисов в большинстве браузеров.

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

doSomething() .then(function(result) < return doSomethingElse(result); >) .then(newResult => doThirdThing(newResult)) .then(() => doFourthThing()) .catch(error => console.log(error));

Обратите внимание что () => x это сокращённая форма () => < return x; >.

Теперь у нас имеется единственная определённая цепочка с правильной обработкой ошибок.

Использование async / await предотвращает большинство, если не все вышеуказанные ошибки, но взамен появляется другая частая ошибка — забыть ключевое слово await .

Смотрите также

  • Promise.then()
  • Спецификация Promises/A+ (EN)
  • Нолан Лоусон (Nolan Lawson): У нас проблемы с промисами - распространённые ошибки (EN)

Found a content problem with this page?

  • Edit the page on GitHub.
  • Report the content issue.
  • View the source on GitHub.

Promise

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

Промис (promise) - это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Асинхронная операция, упрощенно говоря, это некоторое действие, выполняется независимо от окружающего ее кода, в котором она вызывается, не блокирует выполнение вызываемого кода.

Промис может находиться в одном из следующих состояний:

  • pending (состояние ожидания): начальное состояние, промис создан, но выполнение еще не завершено
  • fulfilled (успешно завершено): действие, которое представляет промис, успешно завершено
  • rejected (завершено с ошибкой): при выполнении действия, которое представляет промис, произошла ошибка

Для создания промиса применяется конструктор типа Promise :

new Promise(executor)

В качестве параметра конструктор принимает функцию, которая выполняется при создании промиса. Обычно эта функция представляет асинхронные операции, которые занимают продолжительное время. Например, определим простейший промис:

const myPromise = new Promise(function()< console.log("Выполнение асинхронной операции"); >);

Здесь функция просто выводит на консоль сообщение. Соответственно при выполнении этого кода мы увидим на консоли сообщение "Выполнение асинхронной операции" .

При создании промиса, когда его функция еще не начала выполняться, промис переходит в состояние "pending", то есть ожидает выполнения.

Как правило, функция, которая передается в конструктор Promise, принимает два параметра:

const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); >);

Оба этих параметра - resolve и reject также представляют функции. И каждая из этих функций принимает параметр любого типа.

Первый параметр - функция resolve вызывается в случае успешного выполнения. Мы можем в нее передать значение, которое мы можем получить в результате успешного выполнения.

Второй параметр - функция reject вызывается, если выполнение операции завершилось с ошибкой. Мы можем в нее передать значение, которое представит некоторую информацию об ошибке.

Успешное выполнение промиса

Итак, первый параметр функции в конструкторе Promise - функция resolve выполняется при успешном выполненим. В эту функцию обычно передается значение, которое представляет результат операции при успешном выполнении. Это значение может представлять любой объект. Например, передадим в эту функцию строку:

const myPromise = new Promise(function(resolve)< console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); >);

Функция resolve() вызывается в конце выполняемой операции после всех действий. При вызове этой функции промис переходит в состояние fulfilled (успешно выполнено).

При этом стоит отметить, что теоретически мы можем возвратить из функции результат, но практического смысла в этом не будет:

const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); return "Привет мир!"; >);

Данное возвращаемое значение мы не сможем передать во вне. И если действительно надо возвратить какой-то результат, то он передается в функцию resolve() .

Передача информации об ошибке

Второй параметр функции в конструкторе Promise - функция reject вызывается при возникновении ошибки. В эту функцию обычно передается некоторая информация об ошибке, которое может представлять любой объект. Например:

const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); reject("Переданы некорректные данные"); >);

При вызове функции reject() промис переходит в состояние rejected (завершилось с ошибкой).

Объединение resolve и reject

Естественно мы можем определить логику, при которой в зависимости от условий будут выполняться обе функции:

const x = 4; const y = 0; const myPromise = new Promise(function(resolve, reject) < if(y === 0) < reject("Переданы некорректные данные"); >else < const z = x / y; resolve(z); >>);

В данном случае, если значени константы y равно 0, то сообщаем об ошибке, вызывая функцию reject() . Если не равно 0, то выполняем операцию деления и передаем результат в функцию resolve() .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *