javascript for impatient programmers. глава 38. асинхронные функции [перевод]
Published: 2020-04-22 10:18:32

Вольный перевод главы книги Dr. Axel Rauschmayer "[JavaScript for impatient programmers. Async functions](https://exploringjs.com/impatient-js/ch_async-functions.html)"

## Async функции

Грубо говоря, async *функции* предоставляют улучшенный синтаксис для кода, использующего промисы. Потому, прежде чем использовать их, мы должны разобраться с [промисами](https://exploringjs.com/impatient-js/ch_promises.html).

### 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”](https://exploringjs.com/impatient-js/ch_async-iteration.html#async-generators)). Его операнд обычно является промисов и приводит к следующим шагам:

- Текущая async функция приостановлена и ее значение возвращено. Аналогично работает `yield` в [синхронных генераторах](https://exploringjs.com/impatient-js/ch_sync-generators.html).
- Текущая задача закончена, обработка очереди задач продолжается.
- Когда и если промис будет решен, 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”](https://exploringjs.com/impatient-js/ch_promises.html#focus-on-async-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` для того, чтобы присоединиться к длительной асинхронной операции. Это дает гарантию, что вывод в консоль произойдет *после* ее выполнения.