Вольный перевод главы книги Dr. Axel Rauschmayer "JavaScript for impatient programmers. Async functions"
Async функции
Грубо говоря, async функции предоставляют улучшенный синтаксис для кода, использующего промисы. Потому, прежде чем использовать их, мы должны разобраться с промисами.
38.1 Async функции: основы
Рассмотрим следующую функцию:
async function fetchJsonAsync(url) {
try {
const request = await fetch(url); // async
const text = await request.text(); // async
return JSON.parse(text); // sync
}
catch (error) {
assert.fail(error);
}
}
Синхронно выглядящий код из примера выше эквивалентен коду, который использует промисы напрямую:
function fetchJsonViaPromises(url) {
return fetch(url) // async
.then(request => request.text()) // async
.then(text => JSON.parse(text)) // sync
.catch(error => {
assert.fail(error);
});
}
Несколько наблюдений об асинхронной функции fetchJsonAsync()
:
- Async функция отмечается ключевым словом
async
. - В теле Async функции мы пишем основанный на промисах код, как будто она синхронная. Нужно только применить оператор
await
к значению, являющемуся промисом. Этот оператор приостанавливает Async функцию и возобновляет когда промис вернет ответ:- Если промис был выполнен успешно,
await
вернет заполненное значение. - Если промис был выполнен неуспешно,
await
выбросит значение reject`а.
- Если промис был выполнен успешно,
- Результатом Async функции всегда будет являться промис:
- Любое возвращаемое (явное или неявное) значение используется за заполнения промиса.
- Любое выброшенное исключение используется для отклонения промиса.
fetchJsonAsync()
и fetchJsonViaPromises()
вызываются одинаковым способом, так:
fetchJsonAsync('http://example.com/person.json')
.then(obj => {
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
});
Async функции основаны на промисах также, как и функции, использующие их напрямую
Глядя со стороны, практически невозможно найти разницу между возвращаемой промис и асинхронной функцией.
38.1.1 Async конструкции
JavaScript имеет следующие асинхронные версии синхронно вызываемых сущностей. Обычно они выполняют роль реальной функции или метода.
// Асинхронное определение функции
async function func1() {}
// Асинхронное выражение функции
const func2 = async function () {};
// Асинхронная стрелочная функция
const func3 = async () => {};
// Определение асинхронного метода в литерале объекта
const obj = { async m() {} };
// Определение асинхронного метода в определении класса
class MyClass { async m() {} }
Асинхронные функции vs. async функции
Разница между асинхронной функцией и async функцией мала, но важна:
- Асинхронная функция - это функция, которая возвращает результат асинхронно. Например, функция на основе коллбэков или функция на основе промисов.
-
Async функция - определяется специальным синтаксисом, использующим ключевые слова
async
иawait
. Также может называться async/await функцией. Async функции основаны на промисах и, следовательно, асинхронных функциях (что может сбивать с толку).
38.2 Возвращаемые значения async функций
38.2.1 Async функции всегда возвращают промис
Каждая async функция всегда возвращает промис.
Внутри async функции из примера ниже, мы возвращаем выполненный промис с помощью return
(строка A):
async function asyncFunc() {
return 123; // (A)
}
asyncFunc()
.then(result => {
assert.equal(result, 123);
});
И, как обычно, если мы не возвращаем ничего явно, возвращаем undefined
:
async function asyncFunc() {
}
asyncFunc()
.then(result => {
assert.equal(result, undefined);
});
Отклонить результат промиса можно с помощью throw
(строка A):
async function asyncFunc() {
throw new Error('Problem!'); // (A)
}
asyncFunc()
.catch(err => {
assert.deepEqual(err, new Error('Problem!'));
});
38.2.2 Возвращаемый промис не оборачивается в другой промис
Если мы вернем промис p
из async функции, то p
станет ее результатом (или, скорее, результатом, "фиксированном" на p
и ведущим себя как он). То есть, возвращаемый промис не будет обернут в еще один.
async function asyncFunc() {
return Promise.resolve('abc');
}
asyncFunc()
.then(result => assert.equal(result, 'abc'));
Напомним, что любой промис q
рассматривается одинаково в следующих ситуациях:
-
resolve(q)
внутриnew Promise((resolve, reject) => { ··· })
-
return q
внутри.then(result => { ··· })
-
return q
внутри.catch(err => { ··· })
38.2.3 Выполнение async функций: синхронный старт, асинхронный расчет (продвинутый)
Async функции выполняются таким образом:
- Результирующий промис
p
создается на старте async функции. - Выполняется тело функции. Есть два варианта, когда выполнение может выйти из тела функции:
-
Полностью, когда рассчитывает
p
:-
return
выполняетp
. -
throw
отклоняетp
.
-
-
Временно, когда ожидает расчета другого промиса
q
, определенного с помощьюawait
. Async функция приостанавливается, и выполнение выходит из тела. Возобновляется, как толькоq
заполнится.
-
Полностью, когда рассчитывает
- Промис
p
возвращается после того, как выполнение покинуло тело функции в первый раз.
Заметьте, что уведомление о выполнении результирующего p
происходит асинхронно, как и всегда в случае с промисами.
Код ниже показывает: async функция начинается синхронно (строка A), выполняется задачу на строке C, асинхронно заполняется результирующий промис (строка B).
async function asyncFunc() {
console.log('asyncFunc() starts'); // (A)
return 'abc';
}
asyncFunc().
then(x => { // (B)
console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)
// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'
38.3 await
: работа с промисами
Оператор await
может использоваться только внутри async функций или async генераторов (о которых идет разговор в §39.2 “Asynchronous generators”). Его операнд обычно является промисов и приводит к следующим шагам:
- Текущая async функция приостановлена и ее значение возвращено. Аналогично работает
yield
в синхронных генераторах. - Текущая задача закончена, обработка очереди задач продолжается.
- Когда и если промис будет решен, async функция возобновится с новой задачей:
- Если промис выполнен успешно,
await
вернет значение. - Если промис отклонен,
await
выбросить значение rejection`а.
- Если промис выполнен успешно,
Далее описывается, как await
обрабатывает промисы с различными состояниями.
38.3.1 await
и выполненные промисы
В этом случае await
возвращает выполненное значение:
assert.equal(await Promise.resolve('yes!'), 'yes!');
Значения не вида промис тоже позволительны и просто прокидываются (синхронно, без приостановки async функции):
assert.equal(await 'yes!', 'yes!');
38.3.2 await
и отклоненные промисы
В этом случае await
выбрасывает значение rejection`а:
try {
await Promise.reject(new Error());
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
}
Экземпляры Error
(включая экземпляры его подклассов) также обрабатываются и выбрасываются:
try {
await new Error();
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
}
38.3.3 await
на мелководье (мы не можем использовать его в коллбэках)
Если нам нужно приостановить async функцию с помощью await
, мы должны делать это внутри этой функции; и не можем внутри вложенной, вроде коллбэка. Значит, приостановка не работает с глубиной.
Код из примера ниже не будет выполнен:
async function downloadContent(urls) {
return urls.map((url) => {
return await httpGet(url); // SyntaxError!
});
}
Причина в том, что в теле обычной стрелочной функции не может быть await
.
Окей, тогда давайте попробуем стрелочную async функцию:
async function downloadContent(urls) {
return urls.map(async (url) => {
return await httpGet(url);
});
}
Увы, и здесь не сработает. .map()
(и, следовательно, downloadContent()
) возвращает массив промисов, а не массив со значениями (необернутыми значениями).
Одним возможным решением будет использовать Promise.all()
, чтобы развернуть все промисы:
async function downloadContent(urls) {
const promiseArray = urls.map(async (url) => {
return await httpGet(url); // (A)
});
return await Promise.all(promiseArray);
}
Можно ли улучшить этот код? Можно: в строке A мы разворачиваем промис с помощью await
только чтобы сразу же переобернуть его с помощью return
. Если опустить await
, нам больше не понадобится стрелочная async функция:
async function downloadContent(urls) {
const promiseArray = urls.map(
url => httpGet(url));
return await Promise.all(promiseArray); // (B)
}
По той же причине, мы можем опустить await
в строке B.
38.5 Самовызывающаяся стрелочная async функция
Если нам нужен await
вне async функции (например, в верхнем уровне модуля), то для этого подойдет самовызывающаяся стрелочная async функция:
(async () => { // start
const promise = Promise.resolve('abc');
const value = await promise;
assert.equal(value, 'abc');
})(); // end
Ее результатом будет промис:
const promise = (async () => 123)();
promise.then(x => assert.equal(x, 123));
38.6 Параллелизм и await
В двух последующих подсекциях нам понадобится функция-помощник paused()
:
/**
* Резолвить после `ms` миллисекунд
*/
function delay(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms);
});
}
async function paused(id) {
console.log('START ' + id);
await delay(10); // pause
console.log('END ' + id);
return id;
}
38.6.1 await
: последовательный запуск асинхронных функций
Если перед вызовами нескольких асинхронных функций мы добавим await
- они выполнятся последовательно:
async function sequentialAwait() {
const result1 = await paused('first');
assert.equal(result1, 'first');
const result2 = await paused('second');
assert.equal(result2, 'second');
}
// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'
paused('second')
запустится только после того, как завершится paused('first')
.
38.6.2 await
: одновременный запуск асинхронных функций
Если вам нужно запустить несколько функций одновременно, используйте метод Promise.all()
:
async function concurrentPromiseAll() {
const result = await Promise.all([
paused('first'), paused('second')
]);
assert.deepEqual(result, ['first', 'second']);
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'
Обе асинхронные функции запустятся в одно время и, когда обе выполнятся, await
вернет нам либо массив, либо заполненные значения, но, если хоть один из промисов будет отклонен - вернется исключение.
Если мы уделяем внимание не тому, как обрабатывается результаты, а где мы запускаем основанное на промисах вычисление §37.5.2 “Concurrency tip: focus on when operations start” . Следующий код такой же "параллельный", как и из прошлого примера:
async function concurrentAwait() {
const resultPromise1 = paused('first');
const resultPromise2 = paused('second');
assert.equal(await resultPromise1, 'first');
assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'
38.7 Советы по использованию async функций
38.7.1 await
не нужен, когда мы “выбросили и забыли”
await
не обязателен при работе с функциями на промисах; он нужен только когда мы хотим приостановить ее и дождаться выполнения промиса. Если нужно только начать асинхронную операцию, он не нужен:
async function asyncFunc() {
const writer = openFile('someFile.txt');
writer.write('hello'); // не ждать
writer.write('world'); // не ждать
await writer.close(); // ждать
}
В примере выше, мы не ожидаем выполнения метода .write()
, потому что не важно, когда это произойдет. Гораздо важнее, когда выполнится .close()
.
Примечание: каждый вызов метода .write()
начинается синхронно, что предотвращает конкуренцию.
38.7.2 Может иметь смысл await
и игнорировать результат
Иногда может иметь смысл использование await
, даже если мы игнорируем результат. Например:
await longRunningAsyncOperation();
console.log('Done!');
В примере выше мы используем await
для того, чтобы присоединиться к длительной асинхронной операции. Это дает гарантию, что вывод в консоль произойдет после ее выполнения.
Top comments (0)