Главная » Статьи » Интересное объяснение async / await в JavaScript

Интересное объяснение async / await в JavaScript

От автора: в JavaScript вы можете кодировать асинхронные задачи тремя способами. Давайте посмотрим на эти варианты.

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

const callbackFunction = result = { // Called when the operation completes
};
asyncOperation(params, callbackFunction);

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

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

const promise = asyncOperation(params); promise.then(result => { // Called when the operation completes
});

Вы видели цепочки промисов .then().then()…then()..? Проблема промисов — их многословность. Наконец, третий вариант — это синтаксис async/await (начиная с ES2017). Это позволяет вам писать асинхронный код в сжатой и синхронной манере:

(async function() { const result = await asyncOperation(params); // Called when the operation completes
})();

В этом посте я объясню, шаг за шагом, как использовать async/await в JavaScript.

Примечание: async/await – это синтаксический сахар поверх промисов. Я рекомендую ознакомиться с промисами, прежде чем продолжить.

Синхронное добавление

Поскольку в названии поста упоминается интересное объяснение, я собираюсь постепенно объяснять историю async/await. Начнем с простой (синхронной) функции, задача которой рассчитать прибавку к зарплате:

function increaseSalary(base, increase) { const newSalary = base + increase; console.log(`New salary: ${newSalary}`); return newSalary;
} increaseSalary(1000, 200); // => 1200
// logs "New salary: 1200"

increaseSalary() — это функция, которая суммирует 2 числа. n1 + n2 это синхронная операция.

Начальник не хочет быстро повышать зарплату сотруднику. Таким образом, вам не разрешено использовать оператор сложения функции + в increaseSalary().

Вместо этого вы должны использовать медленную функцию, которая требует 2 секунды для суммирования чисел. Назовем функцию slowAddition():

function slowAddition(n1, n2) { return new Promise(resolve => { setTimeout(() => resolve(n1 + n2), 2000); });
} slowAddition(1, 5).then(sum => console.log(sum));
// After 2 seconds logs "6"

slowAddition() возвращает промис, который разрешается в сумму аргументов после задержки в 2 секунды. Как обновить функцию increaseSalary() для поддержки медленного добавления?

Асинхронное добавление

Первая наивная попытка — заменить выражение n1 + n2 вызовом slowAddition(n1, n2):

function increaseSalary(base, increase) { const newSalary = slowAddition(base, increase); console.log(`New salary: ${newSalary}`); return newSalary;
} increaseSalary(1000, 100); // => [object Promise]
// Logs "New salary [object Promise]"

К сожалению, функция increaseSalary() не умеет обрабатывать промисы. Функция рассматривает промисы как обычные объекты: она не знает, как и когда извлекать значения из промисов.

Теперь самое время узнать, как increaseSalary() обрабатывает промис, возвращаемый slowAddition() с помощью синтаксиса async/await.

Во-первых, вам нужно добавить ключевое слово async рядом с объявлением функции. Затем внутри тела функции вам нужно использовать оператор await, чтобы функция ожидала выполнения промиса. Давайте внесем эти изменения в работу increaseSalary():

async function increaseSalary(base, increase) { const newSalary = await slowAddition(base, increase); console.log(`New salary: ${newSalary}`); return newSalary;
} increaseSalary(1000, 200); // => [object Promise]
// After 2 seconds logs "New salary 1200"

JavaScript оценивает const newSalary = await slowAddition(base, increase) следующим образом:

await slowAddition(base, increase) приостанавливает выполнение функции increaseSalary()

Через 2 секунды промис, возвращаемый пользователем slowAddition(base, increase), разрешается.

Возобновляется выполнение функции increaseSalary()

newSalary присваивается разрешенное значение промиса 1200 (1000+200)

Выполнение функции продолжается в обычном режиме.

Проще говоря, когда JavaScript встречается await promise в функции async, он приостанавливает выполнение функции до тех пор, пока промис не будет выполнен. Разрешенное значение промиса становится результатом оценки await promise.

Даже несмотря на то, что return newSalary возвращает число 1200, если вы посмотрите на фактическое значение, возвращаемое функцией increaseSalary(1000, 200) — это все равно промис!

Функция async всегда возвращает промис, который разрешается в значение внутри тела функции return value:

increaseSalary(1000, 200).then(salary => { salary; // => 1200
});

Функции аsync, возвращающая промис — это хорошо, потому что вы можете вкладывать функции async.

Ошибка асинхронного добавления

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

function slowAdditionBroken(n1, n2) { return new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Unable to sum numbers')), 3000); });
} slowAdditionBroken(1, 5).catch(e => console.log(e.message));
// After 3 seconds logs "Unable to sum numbers"

Как обработать отклоненный промис внутри асинхронной функции calculateSalary()? Просто заключите оператор await в блок try/catch:

async function increaseSalaryBroken(base, increase) { let newSalary; try { newSalary = await slowAdditionBroken(base, increase); } catch (e) { console.log(`Error: ${e.message}`); newSalary = base * 2; } console.log(`New salary: ${newSalary}`); return newSalary;
} increaseSalaryBroken(1000, 200);
// After 3 seconds logs // "Error: Unable to sum numbers", "New salary: 2000"

В выражении await slowAdditionBroken(base, increase) JavaScript приостанавливает выполнение функции и ждет, пока промис не будет выполнен (промис успешно разрешен) или не будет отклонен (произошла ошибка).

Через 3 секунды промис отклоняется new Error(‘Unable to sum numbers’). Из-за отклонения выполнение функции переходит в условие catch (e){ }, в котором базовый оклад умножается на 2.

Скупой платит дважды. Теперь босс должен платить двойную зарплату. Хорошо!

Если вы не перехватите отклоненный промис, ошибка распространяется, и промис, возвращаемый функцией async, отклоняется:

async function increaseSalaryBroken(base, increase) { const newSalary = await slowAdditionBroken(base, increase); return newSalary;
} increaseSalaryBroken(1000, 200).catch(e => { e.message; // => "Unable to sum numbers"
});

Вложенные асинхронные функции

Несмотря на выражение return <value> внутри асинхронной функции, возвращающее значение является полезной нагрузкой, а не промисом, тем не менее, когда асинхронная функция вызывается, она возвращает промис. Это хорошо, потому что вы можете вкладывать асинхронные функции!

Например, напишем асинхронную функцию, которая увеличивает массив зарплат с помощью функции slowAddition():

async function increaseSalaries(baseSalaries, increase) { let newSalaries = []; for (let baseSalary of baseSalaries) { newSalaries.push( await increaseSalary(baseSalary, increase); ); } console.log(`New salaries: ${newSalaries}`); return newSalaries;
} increaseSalaries([950, 800, 1000], 100);
// After 6 seconds logs "New salaries: 1050,900,1100"

await salaryIncrease(baseSalary, increase) вызывается 3 раза для каждой зарплаты в массиве. Каждый раз JavaScript ждет 2 секунды, пока не будет вычислена сумма. Таким образом вы можете вкладывать функции async в функции async.

Параллельный async

В предыдущем примере добавления массива зарплат добавление происходит последовательно: функция приостанавливается на 2 секунды для каждой зарплаты.

Но можно повышать зарплату параллельно! Давайте воспользуемся функцией Promise.all(), чтобы начать все повышения зарплаты одновременно:

async function increaseSalaries(baseSalaries, increase) { let salariesPromises = []; for (let baseSalary of baseSalaries) { salariesPromises.push( increaseSalary(baseSalary, increase) ); } const newSalaries = await Promise.all(salariesPromises); console.log(`New salaries: ${newSalaries}`); return newSalaries;
} increaseSalaries([950, 800, 1000], 100);
// After 2 seconds logs "New salaries: 1050,900,1100"

Задачи по увеличению зарплаты начинаются сразу (await не используется рядом с increaseSalary(baseSalary, increase)) и у нас накапливаются промисы salariesPromises.

await Promise.all(salariesPromises) затем приостанавливает выполнение функции до тех пор, пока все асинхронные операции, обрабатываемые параллельно, не завершатся. Наконец, только через 2 секунды переменная newSalaries будет содержать увеличенные зарплаты.

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

Практический пример async

Обычная ситуация, когда вы хотите использовать синтаксис async/await — это получение удаленных данных.

Метод fetch() — хороший кандидат для использования async/await, потому что он возвращает промис, который разрешается в значение, возвращаемое удаленным API.

Например, вот как можно получить список фильмов с удаленного сервера:

async function fetchMovies() { const response = await fetch('https://api.example.com/movies'); if (!response.ok) { throw new Error('Failed to fetch movies'); } const movies = await response.json(); return movies;
}

await fetch(‘https://api.example.com/movies’) собирается приостановить выполнение fetchMovies(), пока запрос не будет завершен. Затем вы можете извлечь фактическое использование await response.json().

Заключение

async/await является синтаксическим сахаром поверх промисов и обеспечивает способ синхронной обработки асинхронных задач. async/await имеет 4 простых правила:

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

Оператор await promise приостанавливает выполнение функции до тех пор, пока promise не будет успешно разрешен или отклонен.

Если promise разрешен успешно, то оператор await возвращает разрешенное значение: const resolvedValue = await promise. В противном случае отклоненный промис можно перехватить внутри try/catch.

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

Тест: является ли ошибкой ожидание примитивных значений, например await 3?

Автор: Dmitri Pavlutin

Источник: https://dmitripavlutin.com

Редакция: Команда webformyself.