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

Что такое await

  • автор:

Async/await

Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.

Асинхронные функции

Начнём с ключевого слова async . Оно ставится перед функцией, вот так:

async function f()

У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.

Например, эта функция возвратит выполненный промис с результатом 1 :

async function f() < return 1; >f().then(alert); // 1

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

async function f() < return Promise.resolve(1); >f().then(alert); // 1

Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await , которое можно использовать только внутри async -функций.

Await

// работает только внутри async–функций let value = await promise;

Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

В этом примере промис успешно выполнится через 1 секунду:

async function f() < let promise = new Promise((resolve, reject) => < setTimeout(() =>resolve("готово!"), 1000) >); let result = await promise; // будет ждать, пока промис не выполнится (*) alert(result); // "готово!" > f();

В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».

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

По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then .

await нельзя использовать в обычных функциях

Если мы попробуем использовать await внутри функции, объявленной без async , получим синтаксическую ошибку:

await

Оператор await используется для ожидания окончания Promise . Может быть использован только внутри async function или на верхнем уровне модуля.

Синтаксис

[rv] = await expression;

Promise или любое другое значение для ожидания разрешения.

Возвращает полученное из Promise значение, либо само значение, если оно не является Promise.

Описание

Оператор await заставляет функцию, объявленную с использованием оператора async , ждать выполнения Promise и продолжать выполнение после возвращения Promise значения. Впоследствии возвращает полученное из Promise значение. Если типом значения, к которому был применён оператор await , является не Promise , то значение приводится к успешно выполненному Promise .

Если Promise отклоняется, то await генерирует исключение с отклонённым значением.

Примеры

await ожидает разрешения Promise и возвращает полученное значение.

function resolveAfter2Seconds(x)  return new Promise((resolve) =>  setTimeout(() =>  resolve(x); >, 2000); >); > async function f1()  var x = await resolveAfter2Seconds(10); console.log(x); // 10 > f1(); 

Если типом значения является не Promise , значение преобразуется к успешно выполненному Promise .

async function f2()  var y = await 20; console.log(y); // 20 > f2(); 

Если Promise отклонён, то выбрасывается исключение с переданным значением.

async function f3()  try  var z = await Promise.reject(30); > catch (e)  console.log(e); // 30 > > f3(); 

Обработка отклонённого Promise без try/catch блока.

var response = await promisedFunction().catch((err) =>  console.log(err); >); // response получит значение undefined, если Promise будет отклонён 

Спецификации

Specification
ECMAScript Language Specification
# sec-async-function-definitions

Поддержка браузерами

BCD tables only load in the browser

See also

  • async function
  • async function expression
  • AsyncFunction object

Found a content problem with this page?

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

This page was last modified on 7 авг. 2023 г. by MDN contributors.

Your blueprint for a better internet.

MDN

Support

  • Product help
  • Report an issue

Our communities

Developers

  • Web Technologies
  • Learn Web Development
  • MDN Plus
  • Hacks Blog
  • Website Privacy Notice
  • Cookies
  • Legal
  • Community Participation Guidelines

Visit Mozilla Corporation’s not-for-profit parent, the Mozilla Foundation.
Portions of this content are ©1998– 2023 by individual mozilla.org contributors. Content available under a Creative Commons license.

Что такое await

Внедение стандарта ES2017 в JavaScript привнесло два новых оператора: async и await , который призваны упростить работу с промисами.

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

async function название_функции() < // асинхронные операции >

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

async function название_функции()

Оператор await приостанавливает выполнение асинхронной функции, пока объект Promise не возвратить результат.

Стоит учитывать, что оператор await может использоваться только внутри функции, к которой применяется оператор async .

Сначала рассмотрим самый простейший пример с использованием Promise:

function sum(x, y)< return new Promise(function(resolve)< const result = x + y; resolve(result); >); > sum(5, 3).then(function(value)< console.log("Результат асинхронной операции:", value); >); // Результат асинхронной операции: 8

В данной случае функция sum() представляет асинхронную задачу. Она принимает два числа и возвращает объект Promise, в котором выполняется сложение этих чисел. Результат сложения передается в функцию resolve() . И далее в методе then() мы можем получить этот результат и выполнить с ним различные действия.

Теперь перепишем этот пример с использованием async/await :

function sum(x, y)< return new Promise(function(resolve)< const result = x + y; resolve(result); >); > async function calculate() < const value = await sum(5, 3); console.log("Результат асинхронной операции:", value); >calculate(); // Результат асинхронной операции: 8

Здесь мы определили асинхронную функцию calculate() , к которой применяется async :

async function calculate()

Внутри функции вызывается асинхронная операция sum() , которой передаются некоторые значения. Причем к этой функции применяется оператор await . Благодаря оператору await больше нет надобности вызывать у промиса метод then() . А результат, который возвращает Promise, мы можем получить напрямую из вызова функции sum и, например, присвоить константе или переменной:

const value = await sum(5, 3);

Затем мы можем вызвать функцию calculate() как обычную функции и тем самым выполнить все ее действия.

calculate();

Выполнение последовательности асинхронных операций

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

function sum(x, y)< return new Promise(function(resolve)< const result = x + y; resolve(result); >); > async function calculate() < const value1 = await sum(5, 3); console.log("Результат 1 асинхронной операции:", value1); const value2 = await sum(6, 4); console.log("Результат 2 асинхронной операции:", value2); const value3 = await sum(7, 5); console.log("Результат 3 асинхронной операции:", value3); >calculate(); // Результат 1 асинхронной операции: 8 // Результат 2 асинхронной операции: 10 // Результат 3 асинхронной операции: 12

Обработка ошибок

Для обработки ошибок, которые могут возникнуть в процессе вызова асинхронной операции применяется конструкция try..catch..finally .

Например, возьмем следующий код с использованием Promise:

function square(str) < return new Promise((resolve, reject) =>< const n = parseInt(str); if (isNaN(n)) reject("Not a number"); else resolve(n * n); >); >; function calculate(str) < square(str) .then(value =>console.log("Result: ", value)) .catch(error => console.log(error)); > calculate("g8"); // Not a number calculate("4"); // Result: 16

Функция square() получает некоторое значение, в промисе это значение преобразуется в число. И при удачном преобразовании из промиса возвращается квадра числа. Если переданное значение не является числом, то возвращаем ошибку.

При вызове функции square() с помощью метода catch() можно обработать возникшую ошибку.

Теперь перепишем пример с использованием async/await :

function square(str) < return new Promise((resolve, reject) =>< const n = parseInt(str); if (isNaN(n)) reject("Not a number"); else resolve(n * n); >) >; async function calculate(str) < try< const value = await square(str); console.log("Result: ", value); >catch(error) < console.log(error); >> calculate("g8"); // Not a number calculate("4"); // Result: 16

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

Принцип работы async/await в JavaScript

Если вам доводилось работать с JavaScript, то вы наверняка встречались с синтаксисом async/await. Эта функциональность позволяет прописывать асинхронную логику синхронным образом, упрощая тем самым её понимание. Некоторым ветеранам JS известно, что async/await – это просто синтаксический сахар для существующего Promises API. Это означает, что в JS должен быть способ реализации функциональности async/await без использования ключевых слов async и await , хоть и более громоздкий. Именно об этом и пойдёт речь в данной статье.

Видео от автора на ту же тему.

▍ Наша цель

Для понимания нашей задачи рассмотрим пример шаблонного кода.

function wait() < return new Promise((resolve, reject) => < setTimeout(() =>< resolve('Timeout resolved'); >, 2000); >); > async function main() < console.log('Entry'); const result = await wait(); console.log(result); console.log('Exit'); return 'Return'; >main().then(result => < console.log(result); >);
Entry // Пауза 2 секунды Timeout resolved Exit Return

Имея приведённый выше код и соответствующий вывод, можем ли мы переписать функцию main() без использования async и await , по-прежнему получив тот же результат? Условия будут следующими:

  • Не использовать цепочки промисов в main() . Это бы сделало задачу тривиальной и увело нас от изначальной цели. Цепочки промисов могут сработать в примере выше, но это не отразит всю суть async/await и решаемой ими задачи.
  • Не изменять сигнатуры функций. Изменение сигнатур функций потребует обновления их вызовов, что вызовет сложности в крупных проектах со множеством взаимозависимых функций. Старайтесь этого не делать.

▍ Песочница

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

function wait() < return new Promise((resolve, reject) => < setTimeout(() =>< resolve('Timeout resolved'); >, 2000); >); > async function main() < console.log('Entry'); const result = await wait(); console.log(result); console.log('Exit'); return 'Return'; >main().then(result => < console.log(result); >);
Entry Timeout resolved Exit Return

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

▍ Подсказка #1: обратите внимание на приостановку и возобновление функции

Как выглядит функциональность async/await на верхнем уровне? Она заключается в приостановке асинхронной функции, когда та встречает инструкцию await , и последующем возобновлении выполнения, когда ожидаемый промис разрешается или выбрасывает ошибку. Здесь логично спросить «Как обработать приостановку/возобновление функции?» Функции стремятся к выполнению до завершения, не так ли?

Существует ли в JS возможность, имитирующая такое поведение? Да, и это генераторы.

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

Я не буду углубляться в возможности генераторов. Более подробную информацию можете найти по прикреплённой ссылке.

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

function* main() < console.log('Entry'); const message = yield 'Result 1'; console.log(message); console.log('Exit'); yield 'Result 2'; return 'Return'; >const it = main(); /** * Сейчас в консоли вывода нет, несмотря на вызов main(). * Вызов .next() для объекта, возвращённого генератором, запускает выполнение. */ console.log(it.next()); /** * Вывод: * Entry * < value: "Result 1", done: false >* * Генератор приостановил выполнение на строке 4 и возобновит его после очередного вызова * .next(). */ console.log(it.next('Message Passing')); /** * .next() также получает аргумент, который становится доступен для инструкции yield, где * приостановился генератор. * * Вывод: * Message Passing * Exit * < value: "Result 2", done: false >*/ console.log(it.next()); /** * Вывод: * < value: "Return", done: true >*/

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

▍ Подсказка #2: когда возобновлять выполнение функции?

Генераторы предоставляют способ приостановки/возобновления функций. Если вспомнить принцип работы async/await, то асинхронная функция приостанавливается, когда встречает инструкцию await . Значит, можно рассматривать эту функцию как генератор и поместить рядом с промисом инструкцию yield , чтобы функция на этом шаге приостанавливалась. Получится так:

function* main() < console.log('Entry'); const result = wait(); yield; console.log(result); console.log('Exit'); return 'Return'; >const it = main(); it.next(); // Запускает выполнение. Когда нужно снова вызвать it.next()?

Но когда генератор должен возобновить выполнение? Это должно произойти, когда разрешится промис рядом с yield . Откуда вызывающий узнает о промисе, если тот находится в генераторе? Можно ли как-то раскрыть этот промис вызывающему, чтобы он мог прикрепить к нему обратный вызов .then() , который будет вызывать .next() для объекта генератора, чтобы тот продолжил выполнение?

Ответом на все эти вопросы будет просто yield промиса, который нужно ожидать, чтобы вызывающий мог использовать этот промис и вызвать .next() , когда тот разрешится.

function* main() < console.log('Entry'); const result = yield wait(); console.log(result); console.log('Exit'); return 'Return'; >const it = main(); it.next().value.then(() => < it.next(); >);

▍ Подсказка #3: сделать данные разрешившегося промиса доступными для генератора

В предыдущем фрагменте кода мы смогли успешно приостановить выполнение функции и возобновить её, когда разрешился промис. Но генератор не получает от промиса разрешившиеся данные. Переменная result в функции main должна содержать Timeout resolved , что мы видим при использовании async/await. Но в нашей реализации она не получает данные, которые предоставляет промис после своего разрешения.

Можно ли как-то передать эти данные генератору? В конце концов у вызывающего есть к ним доступ, поскольку генератор возвращает промис. Так может ли он передать эти данные обратно генератору, когда вызывающий вызывает для объекта генератора .next ? Выше мы уже встречали Message Passing . Функция .next() получает аргумент, оказывающийся доступным для инструкции yield , где генератор был приостановлен. Значит, для передачи данных из разрешившегося промиса мы просто вызываем .next() с этими данными.

function* main() < console.log('Entry'); const result = yield wait(); console.log(result); console.log('Exit'); return 'Return'; >const it = main(); it.next().value.then(resolvedData => < it.next(resolvedData); >);

Внеся это изменение, мы получили простую реализацию async/await без использования async и await . Обратите внимание на функцию main() и сравните её с аналогичной, использующей async . Они поразительно похожи, не так ли? В нашей реализации вместо async function используется function * , а вместо await – ключевое слово yield . В этом и заключается её красота!

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

▍ Подсказка #4: расширение реализации для работы с несколькими инструкциями yield

Следующим шагом будет расширение реализации под работу с произвольным числом команд yield . Вышеприведённый фрагмент работает лишь с одной, поскольку вызывает .next() только после разрешения первого промиса. Но генератор может создавать произвольное число промисов. Как написать абстракцию, которая будет динамически ожидать разрешения любого созданного промиса, а затем вызывать .next() ?

Этой абстракцией может стать функция (например, run ), получающая генератор. Что run должна возвращать? Опять же, если сравнивать с альтернативной функцией, использующей async , то эта функция неявно возвращает Promise , который разрешается, когда она завершает выполнение. Мы можем сымитировать это поведение, возвращая Promise от функции run и разрешая его, только когда генератор закончил своё выполнение.

Ниже показан соответствующий код. Ваша реализация может отличаться.

run(main).then(result => < console.log(result); >); function run(fn, . args) < const it = fn(. args); return new Promise((resolve, reject) =>< // TODO: Вызывать it.next(), пока есть, что yield. >); >

▍ Подсказка #5: вызов .next() произвольное число раз

А теперь сосредоточимся на реализации функции run . Она должна вызывать .next() для объекта генератора, пока для промисов выполняется yield . Можно ли сделать это с помощью циклов? Будет ли такой вариант работать ожидаемым образом при использовании промисов? Конечно, нет. Циклы не подойдут, поскольку тогда .next() будет продолжать вызываться для объекта генератора, не ожидая создания и разрешения промисов. Можно ли реализовать поочерёдное выполнение как-то более удачно?

Да, с помощью рекурсии! Используя рекурсию, мы сможем продолжать вызывать .next() для объекта генератора при каждом разрешении промиса. Каково будет условие выхода или базовый кейс для завершения рекурсии? Она должна прекращаться, когда завершается выполнение генератора. Что возвращает .next() , когда это происходит? Свойство done в возвращаемом объекте устанавливается на true .

function run(fn, . args) < const it = fn(. args); return new Promise((resolve, reject) => < function step() < const result = it.next(); // Условие выхода if (result.done) < return; >result.value.then(resolvedValue => < step(); >); > // Вызов step() для начала рекурсии step(); >); >

Сейчас мы не передаём resolvedValue из промиса обратно в генератор. Для этого нужно сделать так, чтобы функция step получала аргумент. Также обратите внимание, что возвращаемый run промис никогда не разрешается, потому что мы нигде не вызываем функцию resolve() . Когда этот промис должен разрешиться? Когда генератор завершает выполнение, и выполнять больше нечего. С чем тогда должен разрешаться промис? С тем, что возвращает генератор, так как это соответствует поведению асинхронных функций.

function run(fn, . args) < const it = fn(. args); return new Promise((resolve, reject) => < function step(resolvedValue) < const result = it.next(resolvedValue); // Условие выхода if (result.done) < resolve(result.value); return; >result.value.then(resolvedValue => < step(resolvedValue); >); > // Нет необходимости передавать что-либо для начала выполнения генератора. step(); >); >

Вот мы и получили функциональность async/await без async и await !

На этом наша реализация завершается. В ней асинхронные функции представлены как генераторы, а вместо await для ожидания разрешения промисов мы используем инструкции yield . Для разработчика, использующего нашу реализацию, она по-прежнему будет похожей на вариант с async/await и при этом не нарушит два обозначенных в начале статьи условия.

function* main() < console.log('Entry'); const result = yield wait(); console.log(result); const result2 = yield wait(); console.log(result2); // . console.log('Exit'); return 'Return'; >run(main).then(result => < console.log(result); >); function run(fn, . args) < const it = fn(. args); return new Promise((resolve, reject) => < function step(resolvedValue) < const result = it.next(resolvedValue); // Условие выхода if (result.done) < resolve(result.value); return; >result.value.then(resolvedValue => < step(resolvedValue); >); >
Entry Timeout resolved Timeout resolved Exit Return

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

Именно это делают транспиляторы вроде Babel при преобразовании async/await в более старые версии JS, где этой функциональности ещё не было. Если вы взглянете на транспилированный код, то сможете провести множество параллелей с нашей реализацией.

▍ Дальнейшие шаги

Полученная реализация охватывает только успешный путь async/await. Она не обрабатывает сценарии ошибок, когда промис отклоняется. Я хочу оставить эту доработку в качестве упражнения для вас, так как она окажется аналогична реализации успешного пути, и отличаться будут только используемые функции. Для начала взгляните на Generators API, чтобы понять, есть ли способ обратной передачи ошибок в генератор по аналогии с функцией .next() .

▍ Заключение

Когда я впервые пришёл к этой реализации, то был поражён её красотой и простотой. Я знал, что async/await является синтаксическим сахаром, но не знал, как эта функциональность выглядит изнутри. В статье я осветил конкретно этот аспект async/await, а также его связь с промисами и генераторами. Мне нравится временами разбираться в абстракциях, чтобы лучше понять, как всё работает изнутри. Так я узнаю новые интересные концепции. Надеюсь, что моя статья стала для вас мотивирующей, подогрев любопытство и стремление изучать что-либо на практике, а не просто через чтение готовых руководств.

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

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