Если вы читали документацию по async/await в MDN, вы могли встретить это примечание в конце раздела Описание:

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

Обратите внимание на последнюю часть последнего предложения: «async/await похоже на объединение генераторов и промисов».

Это именно то, что скрывается за async/await!

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

Давайте рассмотрим пример с промисами.

Обещать

У вас есть 2 обещания. Вы должны выполнить второй после завершения первого. Возможно, вы делаете это:

const p1= Promise.resolve(5)
p1.then(val => {
    const p2= Promise.resolve(val*10)
    p2.then(val => console.log(val))   //output: 50

Если бы вы хотели сделать этот код более плоским, вы бы сделали это:

const p1 = Promise.resolve(5)
const p2 = p1.then(val => Promise.resolve(val*10))
p2.then(val => console.log(val))   //output: 50

Что, если бы вы могли сделать что-то вроде этого (Представьте, что p1 волшебным образом получает разрешенное значение для первого обещания, а p2 получает разрешенное значение для второго обещания):

const p1 = Promise.resolve(5)
const p2 = Promise.resolve(p1*10)
console.log(p2) //output: 50

Асинхронно/ждите

Вот где на сцену выходит async/await. Если вы пометите функцию как «async» и добавите ключевое слово «await», p1 и p2 всегда будут получать разрешенные значения вместо промисов.

const myAsync = async () => {
  const p1 = await Promise.resolve(5)
  const p2 = await Promise.resolve(p1*10)
  console.log(p2) //output: 50
}

Прежде чем мы углубимся в то, как это работает, давайте посмотрим на генератор.

Генератор

Генератор — это, по сути, ленивая функция, которая «генерирует» значения по «запросу».

В JavaScript вы можете пометить функцию как генератор знаком «*». Это дает функции возможность генерировать значения с помощью ключевого слова yield. Чтобы получить значение, запрашивающая сторона должна вызвать метод next, доступный при вызове функции-генератора. Результатом функции next() является объект с двумя свойствами: value и done. Value хранит возвращаемое значение yield, а done указывает, осталось ли в функции еще какое-либо значение yield.

Пример:

const gen = function* () {
  yield 1
  yield 2
  yield 3
}
const receiver = gen()  //returns an object with a next fn
receiver.next()   //returns an object {value: 1, done: false}
receiver.next() //returns an object {value: 2, done: false}
receiver.next() //returns an object {value:3, done: false}
receiver.next() //returns an object {value: undefined, done: true}

Ключевым выводом здесь является то, что первый yield в функции gen не выполняется до тех пор, пока не будет вызван первый next(). Функция фактически «приостановлена». Как правило, «n-й» yield не выполняется до тех пор, пока не будет вызван «n-й» next().

В этом примере состояние генератора поддерживается внутри. Это означает, что запрашивающая сторона не может изменить значения.

К счастью, есть возможность передавать значения с помощью следующего метода. Единственный момент, на который следует обратить внимание, это то, что значение, переданное в next(), будет присвоено «результату предыдущего выхода». Что это вообще значит?

Давайте посмотрим на пример:

const gen = function* () {
  let a = yield 1
  let b = yield 2 + a
  let c = yield 3 + b
}
const receiver = gen()  //returns an object with a next fn
receiver.next()   //returns an object {value: 1, done: false}
receiver.next(3) //returns an object {value: 5, done: false}
receiver.next(6) //returns an object {value:9, done: false}
receiver.next() //returns an object {value: undefined, done: true}

Если вы посмотрите на второй, мы передаем значение «3». По сути, происходит то, что результату предыдущего yield, т. е. «a», присваивается значение, переданное во второй следующей функции, т. е. 3 . Значение переменной a теперь изменилось с 1 на 3. Точно так же мы переназначаем значение b равным «6». Следовательно, «с» становится 9, что является последним выходом.

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

Давайте уменьшим масштаб на один уровень и попробуем задать вопрос из того, что мы только что узнали о генераторе и обещании:

Что, если бы генератор мог приостановить выполнение некоторого кода до тех пор, пока обещание не завершится (разрешено/отклонено)?

Асинхронный генератор

Теперь, когда мы знаем, что такое генератор, давайте рассмотрим асинхронныйгенератор.

Генератор считается асинхронным, когда он дает асинхронную операцию (например, обещание)

Давайте посмотрим на пример. Скажем, у нас есть 3 промиса: a, b и c. Каждый раз, когда вызывается next(), генератор возвращает промис. Как вы думаете, какой здесь будет порядок исполнения этих Обещаний?

const gen = function* () {
  let a = yield Promise.resolve(5)
  let b = yield new Promise((res, rej) => setTimeout(() => res(6), 3000))
  let c = yield Promise.resolve(7)
}
const receiver = gen()
let p1 = receiver.next(
p1.value.then(val => console.log(val))   //returns 5  
let p2 = receiver.next()
p2.value.then(val => console.log(val))   //returns 6
let p3 = receiver.next()
p3.value.then(val => console.log(val)) //returns 7
receiver.next()

Вот вывод консоли:

Promise {<resolved>: 5}
Promise {<pending>}__proto__: Promise[[PromiseStatus]]: "resolved"[[PromiseValue]]: 6
Promise {<resolved>: 7}__proto__: Promise[[PromiseStatus]]: "resolved"[[PromiseValue]]: 7
{value: undefined, done: true}
5
7
6

Поскольку мы вызываем next() сразу один за другим, промисы начинают выполняться одновременно. В этом случае, поскольку «b» имеет отложенное разрешение (минимум 3 секунды), его значение (6) разрешается в конце, после «a» (5) и «c» (7). Нас также не волнует состояние «выполнено», так как мы знаем, что обещания в конечном итоге будут разрешены.

Но что, если бы у нас было требование, чтобы «b» выполнялась только после разрешения «a», а «c» — только после разрешения «b».

Повторим два факта о генераторе:

  1. Единственный способ приостановить функцию генератора — не вызывать next()
  2. Мы также знаем, что можем передавать значения внутри next(), за исключением самого первого раза

Давайте создадим новую функцию, которая принимает обещание и вызывает метод генератора next() только после разрешения обещания. Другими словами, он «ждет» выполнения обещания:

const await = p => p.then(val => receiver.next(val)

Вот полный рефакторинг кода:

let receiver = null
const await = p => p.then(val => receiver.next(val))
const async = function* () {
    let a = yield await(Promise.resolve(5))
    console.log(a)
    let b = yield await(new Promise((res, rej) =>  setTimeout(() => res(6), 3000)))
    console.log(b)
    let c = yield await(Promise.resolve(7))
    console.log(c)
}
receiver = async()
let promise = receiver.next()) //invoke the first yield
promise.then(val => console.log("resolved all promises:" + val)

Вот вывод консоли из нашего пользовательского async/await:

{value: Promise, done: false}
5
resolved all promises: undefined
6
7

Теперь, как это выглядит по сравнению с нашим родным использованием async/await в ES6:

const gen = async function() {
    let a = await Promise.resolve(5)
    console.log(a)
    let b = await new Promise((res, rej) =>  setTimeout(() => res(6), 3000))
    console.log(b)
    let c = await Promise.resolve(7)
    console.log(c)
    return c
}
let promise = gen()
console.log(promise)
promise.then(val => console.log('resolved all promises:' + val)

Вот вывод консоли:

Promise
5
6
7
resolved all promises: 7

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

Другими словами, async/await обеспечивает синхронное использование между одним или несколькими промисами.

Небольшая разница…

Между нашей пользовательской реализацией и нативной есть небольшая разница. Когда вызывается первый метод next(), возвращается обещание. Но он разрешается сразу после завершения первого и возвращает значение undefined. Это не то, что делает асинхронная функция. Первоначально он возвращает промис, но ждет разрешения всех промисов внутри своей функции, а затем разрешает начальный промис с возвращаемым значением функции.

Давайте немного рефакторим наш код:

Что мы, по сути, сделали, так это обернули наш первоначальный асинхронный метод оболочкой, которая возвращает исходное обещание вызывающей стороне. Это обещание разрешается только после разрешения всех обещаний внутри асинхронной функции. Это достигается путем передачи обратных вызовов res и rej асинхронному генератору через метод next() (номера строк: 37–40).

Вот вывод консоли сейчас:

resolved: 5
resolved: 6
resolved: 7
all promises resolved

Вот jsfiddle, если вы хотите поиграть: https://jsfiddle.net/abh7cr/w50we39d/

Вывод

Async/await — отличное дополнение к ES6. Они делают наш код намного чище при синхронном выполнении промисов. Но важно понимать, что это не замена Promise.all. Promise.all допускает одновременное выполнение промисов, а async/await допускает только синхронное выполнение промисов.