Йоооо, я рада, что ты здесь! Мы обсудим, как победить асинхронный JavaScript! Пытаясь научиться работать с Node.js и создавать некоторые вещи, я обнаружил, что асинхронные вызовы — не самое простое занятие, с которым нужно научиться справляться. Асинхронный JavaScript на самом деле требует много размышлений, чтобы полностью его понять. Я надеюсь проложить путь, который упростит и ускорит понимание того, как работать с асинхронными вызовами. Первоначально это было опубликовано на dev.to, но поскольку это был один из моих самых популярных постов, я решил опубликовать его и на Medium!
Вы можете найти весь код, использованный в этой статье, на GitHub.
В чем проблема?
JavaScript — это синхронный язык, что означает, что он однопоточный, поэтому он выполняет только один блок кода за раз. Проблема возникает, когда мы хотим сделать какой-то асинхронный вызов, который является многопоточным. Проблема в том, что когда наш JavaScript вызывает асинхронную функцию — наш JavaScript продолжает работать, хотя где-то еще выполняется блок кода. Я чаще всего сталкиваюсь с этой проблемой, когда имею дело с запросами API.
Вот пример -
- Блок JavaScript начинается
- Сделан запрос API
- Код JavaScript продолжается И запросы API обрабатываются
- JavaScript использует ответ на запрос до того, как ответ будет возвращен

Заметили две тройки? Это проблема. Код JavaScript продолжает выполняться по мере выполнения запроса. Это означает, что JavaScript может попытаться использовать значение ответа на запрос до того, как оно станет доступным, и мы принимаем L.
Цель
Цель состоит в том, чтобы иметь возможность вызывать асинхронные функции синхронно — вызовы должны ждать, пока одна из них не завершится, прежде чем выполняться:

Что будет выглядеть примерно так, когда есть несколько асинхронных вызовов:
var a = await asyncToGetA(); var b = await asyncToGetB(a); alert(b);
Использование обратных вызовов
Итак, как нам решить эту проблему? Что ж, давайте сначала взглянем на функции обратного вызова, чтобы мы могли взглянуть на потенциальное исправление. Обратные вызовы — это способ сообщить коду запустить функцию после завершения другой функции. Если ваш код не выполняет слишком много асинхронных вызовов, этот вариант можно использовать. Это достигается передачей функции в другую функцию в качестве аргумента, а затем вызовом функции-аргумента в конце той, которой она была передана.
Допустим, у нас есть функция runThisFirst(), которую мы хотим запустить перед другой функцией runThisSecond(). runThisFirst() будет имитировать асинхронный вызов с setTimeout() и установит x на 5. Когда это будет завершено, runThisSecond() запустится. Поскольку мы хотим, чтобы runThisSecond() выполнялась после runThisFirst(), мы собираемся передать ее как функцию обратного вызова:
// Define functions
var runThisFirst = function(callback){
setTimeout(function(){
x = 5;
callback(); // runThisSecond is called
}, 3000);
}
var runThisSecond = function(){
alert(x);
}
// Run functions, pass runThisSecond as the callback argument
var x;
runThisFirst(runThisSecond);
Вы можете запустить этот фрагмент кода на JSFiddle.
Цепочка обратного вызова
Если обратные вызовы решают нашу проблему с асинхронностью, то не можем ли мы просто объединить обратные вызовы в цепочку? Можно, но становится страшно. Существует концепция Ада обратных вызовов, когда код JavaScript обратного звонка превращается в форму пирамиды, что делает его запутанным и трудным для понимания.
Вот минималистичный пример того, как выглядит скелет пирамиды Callback Hell:
function one() {
setTimeout(function() {
console.log('1. First thing setting up second thing');
setTimeout(function() {
console.log('2. Second thing setting up third thing');
setTimeout(function() {
console.log('3. Third thing setting up fourth thing');
setTimeout(function() {
console.log('4. Fourth thing');
}, 2000);
}, 2000);
}, 2000);
}, 2000);
};
Одна из лучших практик программирования — написание читаемого кода, и обратные вызовы могут отвлечь нас от этого, если слишком много цепочек. Чтобы избежать этого, мы рассмотрим Promises и Async/Await.
Обещания
Функция promise — это функция, которая обещает вернуть значение. Это позволяет вам ассоциировать код с асинхронными вызовами, и все это благодаря тому, что асинхронные вызовы отделены от Promise. Здесь мы можем совершать вызовы API. :) Вот как они работают:
var somePromise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function(){
resolve(x); // Return your promise!
}, 3000);
});
Вы можете видеть, что конструктор Promise имеет два параметра: resolve и reject. Если в промисе все идет по плану (ошибок нет), вызывается resolve, который возвращает какое-то значение для промиса. Если возникает ошибка, Promise должен вызвать reject и вернуть ошибку. В этом примере reject не вызывается.
Теперь давайте попробуем запустить что-то, что зависит от этого обещания, чтобы увидеть, ожидает ли оно разрешения значения x перед выполнением. Мы можем сделать это с помощью функции .then:
var somePromise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function(){
resolve(x); // Return your promise!
}, 3000);
});
somePromise.then((somePromisesReturnValue) => {
alert("Check it out: " + somePromisesReturnValue);
});
Вы можете запустить этот фрагмент кода на JSFiddle.
Проверьте это! Вещи уже выглядят чище и проще для понимания. Хорошая работа. :) Но что, если один промис зависит от другого промиса? Нам придется связать промисы вместе.
Чтобы передать значения из одного промиса в другой, мы собираемся обернуть промис в функцию следующим образом:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
Цепочка обещаний
Теперь мы можем написать другое обещание, anotherPromise(), которое будет принимать возвращаемое значение somePromise() и добавлять к нему 1. Эта функция будет иметь более короткий setTimeout(), поэтому мы можем сказать, что она ожидает разрешения somePromise() перед запуском. Обратите внимание, как мы передаем somePromisesReturnValue в качестве аргумента:
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
Теперь все, что нам нужно сделать, это использовать функцию .then для синхронного вызова этих промисов:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
somePromise().then(anotherPromise);
Вы можете запустить этот фрагмент кода на JSFiddle.
Черт, да! Вы можете видеть, что anotherPromise() ждал возвращаемого somePromise() значения 5, прежде чем выполнить свой код. Дела действительно идут в гору. :)
Асинхронный/ожидание
Потрясающий! Итак, мы закончили, верно? Нет, но мы рядом! Если мы возьмем наш код из последнего раздела и попытаемся присвоить возвращаемое значение из цепочки промисов, мы увидим, что остальная часть кода не ожидает разрешения всей цепочки промисов. «[object Promise]» предупреждается первым.
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
var chainValue = somePromise().then(anotherPromise);
alert(chainValue); // This is executing before chainValue is resolved
Вы можете запустить этот фрагмент кода на JSFiddle.
Как заставить остальной код ждать?! Вот тут-то и появляются async и await. Объявление функции async определяет асинхронную функцию, функцию, которая может выполнять асинхронные вызовы. Оператор await используется для ожидания разрешения промиса, его можно использовать только внутри функции async.
Миссия выполнена
Вместо использования .then давайте создадим функцию main(), чтобы мы могли совершать вызовы, подобные цели, которую мы поставили перед собой в начале статьи:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
const main = async () => {
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
main();
Вы можете запустить этот фрагмент кода на JSFiddle.
Посмотрите, какая красивая основная функция :’) красивая. И вот она, красивая основная функция, которая не является пирамидой. Поздравляем!
Добавление широкой обработки ошибок
Возможно, вы захотите добавить некоторую обработку ошибок в сами промисы при использовании обратного вызова reject, но вы также можете добавить общую обработку ошибок с помощью try/catch внутри функции main(), которая будет перехватывать любые ошибки, возникающие во время выполнения. весь код, используемый в функции main():
const main = async () => {
try{
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
catch(err){
alert('Oh no! Something went wrong! ERROR: ' + err);
}
}
Мы можем проверить это, выдав ошибку в нашем anotherPromise():
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
throw 3292; // ERROR CODE BEING THROWN HERE
setTimeout(function() {
resolve(y);
}, 1000);
});
return promise;
}
const main = async () => {
try{
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
catch(err){
alert('Oh no! Something went wrong! ERROR: ' + err);
}
}
main();
Вы можете запустить этот фрагмент кода на JSFiddle.
Обзор
Я рад, что мы смогли зайти так далеко и найти довольно простой способ преодоления проблем с асинхронностью JavaScript! Мы рассмотрели исправление асинхронных проблем с обратными вызовами, которые могут работать, если не слишком сложно. Затем мы углубились в решение проблемы с объединением Promises и Async/Await! Наконец, мы говорили о том, как в целом обрабатывать ошибки. Если вы хотите узнать больше об обработке ошибок с помощью Promises и Async/Await, я предлагаю вам ознакомиться с документацией: Promise.prototype.catch() и await. Если вы хотите поработать над чем-то, где эта асинхронная функциональность может быть полезна, подумайте о том, чтобы прочитать мою статью о том, как создать бота для Twitter с помощью Node.js. :)
Первоначально опубликовано наdev.to