Углубляемся в JavaScript: всё ли может async/await, или когда использовать Promise

visibility 704
29 Дек 2022г. в 01:16

Что такое async/await и promise?
Прежде чем ответить на поставленный вопрос, нам необходимо узнать немного теории.

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

Асинхронность нужна нам, чтобы делать несколько операций и функций параллельно. Асинхронное программирование — это инструмент для оптимизации высоконагруженных сайтов с долгими и частыми ожиданиями обратной связи. Например, когда одна функция создаёт canvas на странице, другая функция может подготовить данные необходимые для отрисовки внутри canvas. Ещё пример, когда пользователь кладет товар в корзину ему не обязательно ждать ответа сервера, мы заранее можем показать анимацию добавления товара в корзину, а всю остальную логику проверок сделать после ответа сервера, не блокируя интерфейс пользователю.

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

Async/await — синтаксис работающий с промисами, придуман как альтернатива синтаксису промисов. Используя async, можно полностью избежать использования цепочек промисов с помощью await. Async создает Promise. А await ждет выполнения промиса.

Promise — обертка (класс, для простоты понимания) для отложенных и асинхронных вычислений. Ожидает выполнения колбэк функций и никак иначе. Есть два колбэка: один заявляет об успешном выполнении, другой об ошибке. Promise может находиться в трёх состояниях: ожидание (pending), исполнено (fulfilled), отклонено (rejected). Промис начинает выполняться когда мы вызываем метод .then.

Давайте посмотрим практические маленькие примеры синтаксиса.

Пример 1:

(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const json = await response.json();
console.log(json);
})();

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))

Пример 2:

let data = [];

const myFunction = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve([1,2,3]);
}, 3000);
});
};

(async () => {
data = await myFunction();
console.log('выполнится позже', data);
})();

console.log('выполнится первым', data);

let data = [];

const promise = new Promise(resolve => {
setTimeout(() => {
resolve([1,2,3]);
}, 3000);
});

promise.then(value => {
data = value;
console.log('выполнится позже', data);
});

console.log('выполнится первым', data);

Пример 3:

const loadData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const json = await response.json();
return json;
} catch (error) {
throw error;
}
}

(async () => {
try {
const data= await loadData();
console.log(data);
} catch (error) {
console.error(error);
}
})();

const loadData = new Promise((resolve, reject) => {
fetch('https://jsonplaceholder.typicode.com/todos/1').then(response => {
return response.json();
}).then(data => {
resolve(data);
}).catch(error => {
reject(error);
});
});

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

Так как async является надстройкой над промисами, то мы можем смешивать код, например так:

const peopleCount = async () => {
return 1;
}

peopleCount().then(console.log); // 1

или так

const peopleCount = new Promise(resolve => {
resolve(1);
});

console.log(await peopleCount); // 1

Что делать с ошибкой «Heap out of memory» в JavaScript
Что делать с ошибкой «Heap out of memory» в JavaScript
tproger.ru

Плюсы и минусы в теории
Async/await
Плюсы

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

Легко наткнуться на избыточное ожидание последовательного кода. Для истинной параллельности нужно модифицировать код.
Неочевидность возвращаемых значений try…catch.
Promise
Плюсы

Использует традиционный подход колбэков.
Данные с ошибками и данные с успешным результатом операции однозначно понимаемы.
Возможность использовать Promise.all без оглядки на синтаксис.
Оповещения Promise.resolve и Promise.reject доступны везде.
Наглядное использование метода Promise.finally.
Минусы

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


На примерах выше видно, что Promise субъективно является более чистым кодом. Более того я заранее заложил одну противную пакость в примерах, о которой расскажу позже. Эта особенность не позволяет выбранному нами синтаксису использовать асинхронность в полной мере. Кто её нашел сходу, может дальше не читать ?

Вера в обещание
Мое знакомство с асинхронным js-кодом началось с библиотеки «КриптоПро ЭЦП Browser plug-in». Те, кто сталкивался с данной библиотекой, должны меня понять, у меня не было выбора, я искренне влюбился в промисы ? Она вся утыкана промисами. И первой техникой, которой пришлось овладеть, были .then и .catch. Порой вложенность кода составляла 10-15 уровней .then. Спустя годы я понимаю почему разработчикам плагина пришлось так поступить, но она прекрасна в своей ужасности.

Шло время, навыки оттачивались, и с тех пор я всегда пишу js-код на промисах.

А теперь о пакости.

Сложный кейс с промисами, и главное преимущество промисов — колбеки
Попалась мне интереснейшая задача «Платежная система отвечает об успешной оплате не сразу, поэтому придется слать несколько запросов в течение 30 секунд, при этом держать пользователя в режиме прелоадера, при этом если оплата пройдет раньше чем 30 секунд, то из цикла нужно выйти и отключить прелоадер, и если за 30 секунд ответа не получено, то показать ошибку».

Архитектура:

Интервал запросов к серверу 1 секунда.
Необходим один большой (глобальный) промис, чтобы было удобно отключить прелоадер.
До входа в асинхронный код нужно включить прелоадер.
Внутри асинхронного кода должно произойти «нечто ужасное» без потери читабельности.
Отключение прелоадера должно происходить в финальном коде, независимо от того, успешная оплата, или ошибка, и независимо от того, сколько промисов будет использоваться в асинхронном коде.
Для удобства сопоставления алгоритма и архитектуры код совсем чуть-чуть упрощен, и совпадает с оригинальным на 90%.

Результат:

createOrder() { // вызывается при нажатии на кнопку
// блокируем форму от ввода данных (изменение inputs недоступно)
this.lockForm = true;

this.createStripeOrder({ // ajax запрос на бэк
paymentMethod: this.paymentMethodForBackend,
}).then(orderData => {
return this.confirmCardPayment(orderData.stripePaymentIntentClientSecret); // “нечто ужасное” вынесено в отдельный promise
}).then(() => {
this.changeShowSuccessPopup(true);
this.resetCart(); // получим актуальный статус корзины после успешной оплаты (корзина будет пустая)
}).catch(error => { // new Error
this.changeShowFailedPopup({
isShow: true,
message: error.message || (error.data && error.data.message),
});
}).finally(() => {
this.lockForm = false; // отключаем прелоадер
});
},

confirmCardPayment(secret) {
return this.stripe.instance.confirmCardPayment(secret)
.then(result => new Promise((resolve, reject) => {
if (result.error) {
reject(new Error(result.error.message)); // catch
} else if (result.paymentIntent.status === 'succeeded') {
// прокидываем result дальше потому что в он требуется других местах
resolve(result); // then
} else if (result.paymentIntent.status === 'pending') {
// если мы сюда попали значит оплата ещё происходит
// запускаем цикл на получение реального статуса оплаты по номеру заказа,
// раз в секунду, ограничение на кол-во запросов - 15 раз

let counter = 0;
let timer = null;

const stopRepeatIfNeeded = () => {
counter += 1;

if (counter > 14) {
clearInterval(timer);
reject(new Error('Timeout for payment exceeded'));
}
};

timer = setInterval(() => {
this.checkPaymentStatus().then(orderData2 => {
// если completed - заказ оплатился
if (orderData2.status === 'completed') {
resolve(result); // then
} else if (orderData2.status === 'pending') {
// pending - продолжаем цикл
stopRepeatIfNeeded();
} else {
// любой другой статус, значит ошибка оплаты
reject(new Error('Payment status: ' + orderData2.status)); // catch
}
}).finally(() => {
stopRepeatIfNeeded();
});
}, 1000);
}

reject(new Error('Unknown error')); // catch
}));
},

На данном примере видно как используются колбэки — у нас есть полный простор в передаче ошибок в родительский промис, их множественный вызов в разных местах, когда нам необходимо. И также максимальный простор для выбора момента уведомления «родителя» об успешном окончании, тем самым мы решаем 5 пункт из запланированной архитектуры.

Требование заказчика
История закончилась бы замечательно, если бы не одно «но». ТЗ требовало, чтобы весь код был написан на async/await. Глядя на код выше можно сказать, что это достаточно сложный кейс. Первое, что можно подумать: «Это невозможно! Ведь async/await не могут ждать колбека, они только выполняют код и ничего не ждут.»

Ну хорошо… требование заказчика — закон… переписываем.

async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},

async checkOrderPaymentStatus(orderData) {
if (orderData.status === 'completed' || orderData.status === 'succeeded') return true;

if (orderData.status === 'pending') {
// если мы сюда попапали значит оплата ещё происходит
// запускаем цикл на получение реального статуса оплаты по номеру заказа,
// раз в секунду, ограничение на кол-во запросов - 15 раз

for (let i = 0; i < 10; i += 1) {
await this.sleep(2500);

const orderData2 = await this.checkPaymentStatus();

// если completed, то заказ оплатился
if (orderData2.status === 'completed') return true;

if (orderData2.status !== 'pending') {
// какой-то неизвествестный статус оплаты
throw new Error('Payment status: ' + orderData2.status);
}
}

throw new Error('Timeout for payment exceeded');
} else {
// какой-то неизвествестный статус оплаты
throw new Error('Unknown error');
}
},

async createOrder() {
// блокируем форму от ввода данных (изменение inputs недоступно)
this.lockForm = true;

try {
const orderData = await this.createStripeOrder({
paymentMethod: this.paymentMethodForBackend,
});

this.setOrderNumber(orderData.number); // потребуется при показе successPopup и упрощения запроса vuex checkPaymentStatus

const result = await this.stripe.instance.confirmCardPayment(orderData.stripePaymentIntentClientSecret);

if (result.error) {
throw new Error(result.error.message);
} else {
await this.checkOrderPaymentStatus(orderData);

this.changeShowSuccessPopup(true);
this.resetCart(); // получим актуальный статус корзины после успешной оплаты (корзина будет пустая)
}
} catch (error) {
this.changeShowFailedPopup({
isShow: true,
message: error.message || (error.data && error.data.message),
});
}

this.lockForm = false;
},

Задача действительно оказалась не решаема на уровне async/await. Потому что async, await и timeout не работают в связке. Пришлось совсем чуть-чуть смешать два разных синтаксиса с помощью функции sleep. Хорошо это или плохо? Вопрос субъективный. Мы лишь в очередной раз убедились, что async/await является лишь надстройкой над промисами.

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



Оставить комментарий

Ваше имя::


Комментарий::




RobertNef (2023-07-25 08:28:53)
Жителей в Москве не было, и солдаты, как вода в песок, всачивались в нее и неудержимой звездой расплывались во все стороны от Кремля, в который они вошли прежде всего https://ot-ido.art/work/view?id=4227 Солдаты кавалеристы, входя в оставленный со всем добром купеческий дом и находя стойла не только для своих лошадей, но и лишние, все таки шли рядом занимать другой дом, который им казался лучше https://ot-ido.art/work/view?id=5178 Многие занимали несколько домов, надписывая мелом, кем он занят, и спорили и даже дрались с другими командами https://ot-ido.art/work/artfit?id=3610 Не успев поместиться еще, солдаты бежали на улицу осматривать город и, по слуху о том, что все брошено, стремились туда, где можно было забрать даром ценные вещи https://ot-ido.art/work/artfit?id=2968 Начальники ходили останавливать солдат и сами вовлекались невольно в те же действия https://ot-ido.art/work/artfit?id=2731 В Каретном ряду оставались лавки с экипажами, и генералы толпились там, выбирая себе коляски и кареты https://ot-ido.art/work/artfit?id=6397 Остававшиеся жители приглашали к себе начальников, надеясь тем обеспечиться от грабежа https://ot-ido.art/work/genre?id=1&page=232&per-page=12 Богатств было пропасть, и конца им не видно было везде, кругом того места, которое заняли французы, были еще неизведанные, незанятые места, в которых, как казалось французам, было еще больше богатств https://ot-ido.art/artist/works?id=54 И Москва все дальше и дальше всасывала их в себя https://ot-ido.art/work/artfit?id=6930 Точно, как вследствие того, что нальется вода на сухую землю, исчезает вода и сухая земля точно так же вследствие того, что голодное войско вошло в обильный, пустой город, уничтожилось войско, и уничтожился обильный город и сделалась грязь, сделались пожары и мародерство https://ot-ido.art/work/artfit?id=3876 Окончив свой рассказ об обворожительной польке, капитан обратился к Пьеру с вопросом, испытывал ли он подобное чувство самопожертвования для любви и зависти к законному мужу https://ot-ido.art/work/artfit?id=11314 Морель подал свечи и бутылку вина https://ot-ido.art/work/view?id=562 Капитан посмотрел на Пьера при освещении, и его, видимо, поразило расстроенное лицо его собеседника https://ot-ido.art/work/genre?id=18 Рамбаль с искренним огорчением и участием в лице подошел к Пьеру и нагнулся над ним https://ot-ido.art/artist/works?id=1039 В древности китайцы очень часто выдалбливали в горах пещеры-храмы https://ot-ido.art/work/view?id=1898 Подобная традиция возникла под влиянием индийской культуры https://ot-ido.art/work/artfit?id=4477 Впервые в Китае подобные пещеры начали делать во времена правления династий Цзин и Вэй, а также Южных и Северных https://ot-ido.art/work/genre?id=1&page=182&per-page=12 В самых ранних Лунмэнских и Юнганских пещерах можно видеть огромное количество высокоэстетичных изваяний https://ot-ido.art/work/artfit?id=10341 В Юнганских, например, 51 тысяча статуй https://ot-ido.art/work/artfit?id=7207 Самая большая из них достигает высоты в семнадцать метров https://ot-ido.art/work/artfit?id=7197 Всего в Китае имеется около 120 пещер-храмов https://ot-ido.art/artists?search_by_letter=а&page=4 Большая нарядная ваза начала 20-го века https://ot-ido.art/work/artfit?id=8148 Очень распространенные в то время композиции из бисквита часто оставались без клейм, только с серийными номерами фабрики https://ot-ido.art/work/artfit?id=8796 Эта чудесная композиция очень подойдет в детскую, подростковую комнату или спальню https://ot-ido.art/work/artfit?id=6417 Высота 25, ширина 17, длина 24 Физическое состояние Пьера, как и всегда это бывает, совпадало с нравственным https://ot-ido.art/work/view?id=4769 Непривычная грубая пища, водка, которую он пил эти дни, отсутствие вина и сигар, грязное, неперемененное белье, наполовину бессонные две ночи, проведенные на коротком диване без постели, – все это поддерживало Пьера в состоянии раздражения, близком к помешательству https://ot-ido.art/work/artfit?id=10205
AnthonyKax (2023-07-25 10:19:42)
Вся представленная на сайте информация, касающаяся характеристик продуктов, наличия на складе, стоимости товаров, носит информационный характер и ни при каких условиях не является публичной офертой, определяемой положениями Статьи 437(2) Гражданского кодекса Российской Федерации https://crystal-tr.ru/product/liquidimplant-subcutis/ Погружение Режимы предстерилизационной очистки https://crystal-tr.ru/product/jekspress-test-na-vyjavlenie-antigena-k-co/ Самаровка"* На этапе замачивания изделий в растворе https://crystal-tr.ru/product/loktevoj-dozator-mdu-07/ ВИЧ- инфекцию) и грибковой https://crystal-tr.ru/product/bint-nesterilnyj-2/ На этапе замачивания изделий в растворе https://crystal-tr.ru/product/shiny-kstl/ ВИЧ- инфекцию) и грибковой https://crystal-tr.ru/product/kontejner-polimernyj-100-ml/ Раствор дезинфицирующего средства нельзя использовать до предельного срока применения, если не был произведен контроль активности рабочих растворов при помощи тест-полосок https://crystal-tr.ru/product/individualnyj-perevjazochnyj-paket-ipp-1/ Количество и концентрация дезинфицирующих растворов учитываются и при выборе метода дезинфекции https://crystal-tr.ru/product/bespribornaja-jekspress-diagnostika-h-7/ Механический метод подразумевает удаление микроорганизмов с тела человека, инструментария и поверхностей путем мытья, стирки, влажной уборки или вентиляции https://crystal-tr.ru/product/plastyr-na-netkanevoj-bumazhnoj-osnove-1-0-h-500-sm-30-sht/ Это наиболее простой способ дезинфекции, и его часто используют для предварительной очистки медицинских инструментов https://crystal-tr.ru/product/bumaga-dlja-medicinskih-registrirujushhih-priborov-sony-upp-84hg/ Под полной профилактической дезинфекцией https://crystal-tr.ru/product/lejkoplastyr-povjazka-sfm-30-0-sm-h-10-0-sm-50-sht/ При проведении полной https://crystal-tr.ru/product/bespribornaja-jekspress-diagnostika-h-3/ Самаровка" для https://crystal-tr.ru/product/kasseta-15h30-kodak-x-omatic-s-jekranom-lanex-medium/ обработки поверхностей, кресел и сидений из винилис- кожей и 2% раствор для https://crystal-tr.ru/product/podguzniki-dlja-vzroslyh-predo-abult-standartnaja-pachka-razmer-l/ Вагоны рестораны и буфеты пассажирских https://crystal-tr.ru/product/maska-kn95-bez-klapana-vydoha-ffp2/ В пунктах формирования и https://crystal-tr.ru/product-category/consumables/koljushhie/igly-dlja-mezoterapii/ Изделия из стекла и металлов отмывают последовательно в двух водах Лизоформина 3000, в трех водах (Дезформ) по 5 минут, изделия из пластмасс и резины – в двух водах (Лизоформин – 3000), в трех водах (Дезформ) по 10 минут https://crystal-tr.ru/product/gag-complex-dvl-e-formula-mesopharm/ Каналы изделий отмывают с помощью шприца или водоструйного насоса в течение 3-5 минут в каждой емкости, не допуская попадания пропущенной воды в емкость с отмываемыми изделиями https://crystal-tr.ru/product/lejkoplastyr-kinezio-tejp-10-sm-h-500-sm-rozovyj-sfm/
chaihaibe (2023-07-30 02:04:38)
интернет магазин доставки чая или шу пуэр прессованный https://zavaristika.ru/catalog/shen-puer-zelenyj чай улун
CharlesBOW (2023-08-01 02:38:23)
550х1900 600х1900 600х2000 700х2000 800х2000 900х2000 950х2100 950х2200 https://www.dvervam.ru/catalog/interior-doors/sodruzhestvo/lira-3-pvh/ Приобрести выгодно межкомнатные двери со склада с доставкой и установкой — значит совершить рациональную инвестицию в комфорт, уют и безопасность помещения https://www.dvervam.ru/catalog/metallicheskie-dveri/luxor12/vkhodnye-dveri-/luxor-45./ Наша компания предлагает огромный ассортимент качественных и надёжных дверных конструкций, доступная цена на которые обусловлена прямым сотрудничеством с проверенными производителями https://www.dvervam.ru/catalog/metallicheskie-dveri/labirint/vnutrennie-paneli/03-orekh-premium-/ За счёт отсутствия посреднических услуг стоимость каждой представленной модели устанавливается на минимальном уровне, приемлемом для любого бюджета https://www.dvervam.ru/catalog/metallicheskie-dveri/labirint/vnutrennie-paneli/25-beton-svetlyy-chernaya-vstavka-/ Максимально ориентируясь на потребности клиентов, мы предлагаем только хорошие и качественные, надёжные и практичные дверные конструкции, которые в кратчайшие сроки поставляем по Москве и области , а также во все в регионы страны https://www.dvervam.ru/catalog/furniture/razdvizhnye-sistemy/verkhnyaya-napravlyayushchaya-l1-3m/ Мы можем уверенно гарантировать, что приобретенные у нас двери станут достойным элементом и надёжной защитой для Вашего коттеджа, квартиры, частного дома, а также торгового, офисного и другого помещения https://www.dvervam.ru/catalog/interior-doors/ Если вы решили купить новые межкомнатные двери, воспользуйтесь формой выбора дверной конструкции на нашем сайте https://www.dvervam.ru/catalog/furniture/dvernye-ruchki/dvernye-ruchki-v40q/ Благодаря удобному подбору вы можете подобрать лучший вариант на рынке по таким параметрам, как стоимость, цвет, внешний вид, покрытие двери, дизайн полотна, размер дверного проема, механизм открывания и др https://www.dvervam.ru/catalog/interior-doors/sodruzhestvo/s-57-cagrovie/ В ассортименте магазина представлены самые современные модели межкомнатных дверей разной конструкции, назначения и дизайна, изготовленные из различных материалов: Классика – мой любимый стиль https://www.dvervam.ru/catalog/metallicheskie-dveri/labirint/vnutrennie-paneli/zerkalo-maksimum-sandal-belyy/ Менеджер компании : в меру строгие, красивые и при этом эффектные https://www.dvervam.ru/catalog/metallicheskie-dveri/labirint/vnutrennie-paneli/zerkalo-maksimum-beton-svetlyy/ Кроме того мне повезло, действовала акция и мне сделали значительную скидку, так что я не только приобрёл отличные двери, но и сэкономил значительную сумму https://www.dvervam.ru/catalog/interior-doors/sitydoors/sity-2-ultra/ Спасибо за отличное предложение! Служат для звукоизоляции помещения и создания личного пространства https://www.dvervam.ru/catalog/furniture/dvernye-ruchki/dvernye-ruchki-v64d/ В отличие от входных полотен, у межкомнатных конструкций взломостойкость отсутствует https://www.dvervam.ru/politika-konfidentsialnosti.php Обладают небольшой прочностью и толщиной полотна https://www.dvervam.ru/services/ Часто дополняются вставками из стекла или даже бумаги, несущими чисто декоративные функции https://www.dvervam.ru/catalog/metallicheskie-dveri/ Имеют небольшой вес https://www.dvervam.ru/catalog/interior-doors/krashennye-dveri-emal-bp-doors-/birel-/ Полотно можно снять с петель самостоятельно https://www.dvervam.ru/catalog/metallicheskie-dveri/labirint/vnutrennie-paneli/zerkalo-maksimum-beton-svetlyy/ Межкомнатные двери отличаются большой ремонтопригодностью https://www.dvervam.ru/catalog/interior-doors/luxor/art-3/