DEV Community

собачья будка
собачья будка

Posted on • Edited on

javascript for impatient programmers. глава 38. асинхронные функции [перевод]

Вольный перевод главы книги 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Синхронно выглядящий код из примера выше эквивалентен коду, который использует промисы напрямую:

function fetchJsonViaPromises(url) {
  return fetch(url) // async
  .then(request => request.text()) // async
  .then(text => JSON.parse(text)) // sync
  .catch(error => {
    assert.fail(error);
  });
}
Enter fullscreen mode Exit fullscreen mode

Несколько наблюдений об асинхронной функции 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',
  });
});
Enter fullscreen mode Exit fullscreen mode

Async функции основаны на промисах также, как и функции, использующие их напрямую

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

38.1.1 Async конструкции

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

// Асинхронное определение функции
async function func1() {}

// Асинхронное выражение функции
const func2 = async function () {};

// Асинхронная стрелочная функция
const func3 = async () => {};

// Определение асинхронного метода в литерале объекта
const obj = { async m() {} };

// Определение асинхронного метода в определении класса
class MyClass { async m() {} }
Enter fullscreen mode Exit fullscreen mode

Асинхронные функции 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);
});
Enter fullscreen mode Exit fullscreen mode

И, как обычно, если мы не возвращаем ничего явно, возвращаем undefined:

async function asyncFunc() {
}

asyncFunc()
.then(result => {
  assert.equal(result, undefined);
});
Enter fullscreen mode Exit fullscreen mode

Отклонить результат промиса можно с помощью  throw (строка A):

async function asyncFunc() {
  throw new Error('Problem!'); // (A)
}

asyncFunc()
.catch(err => {
  assert.deepEqual(err, new Error('Problem!'));
});
Enter fullscreen mode Exit fullscreen mode

38.2.2 Возвращаемый промис не оборачивается в другой промис

Если мы вернем промис p из async функции, то p станет ее результатом (или, скорее, результатом, "фиксированном" на  p и ведущим себя как он). То есть, возвращаемый промис не будет обернут в еще один.

async function asyncFunc() {
  return Promise.resolve('abc');
}

asyncFunc()
.then(result => assert.equal(result, 'abc'));
Enter fullscreen mode Exit fullscreen mode

Напомним, что любой промис 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'
Enter fullscreen mode Exit fullscreen mode

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!');
Enter fullscreen mode Exit fullscreen mode

Значения не вида промис тоже позволительны и просто прокидываются (синхронно, без приостановки async функции):

assert.equal(await 'yes!', 'yes!');
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Экземпляры Error (включая экземпляры его подклассов) также обрабатываются и выбрасываются:

try {
  await new Error();
  assert.fail(); // we never get here
} catch (e) {
  assert.equal(e instanceof Error, true);
}
Enter fullscreen mode Exit fullscreen mode

38.3.3 await на мелководье (мы не можем использовать его в коллбэках)

Если нам нужно приостановить async функцию с помощью await, мы должны делать это внутри этой функции; и не можем внутри вложенной, вроде коллбэка. Значит, приостановка не работает с глубиной.

Код из примера ниже не будет выполнен:

async function downloadContent(urls) {
  return urls.map((url) => {
    return await httpGet(url); // SyntaxError!
  });
}
Enter fullscreen mode Exit fullscreen mode

Причина в том, что в теле обычной стрелочной функции не может быть await.

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

async function downloadContent(urls) {
  return urls.map(async (url) => {
    return await httpGet(url);
  });
}
Enter fullscreen mode Exit fullscreen mode

Увы, и здесь не сработает. .map() (и, следовательно, downloadContent()) возвращает массив промисов, а не массив со значениями (необернутыми значениями).

Одним возможным решением будет использовать Promise.all(), чтобы развернуть все промисы:

async function downloadContent(urls) {
  const promiseArray = urls.map(async (url) => {
    return await httpGet(url); // (A)
  });
  return await Promise.all(promiseArray);
}
Enter fullscreen mode Exit fullscreen mode

Можно ли улучшить этот код? Можно: в строке A мы разворачиваем промис с помощью await только чтобы сразу же переобернуть его с помощью return. Если опустить await, нам больше не понадобится стрелочная async функция:

async function downloadContent(urls) {
  const promiseArray = urls.map(
    url => httpGet(url));
  return await Promise.all(promiseArray); // (B)
}
Enter fullscreen mode Exit fullscreen mode

По той же причине, мы можем опустить await в строке B.

38.5 Самовызывающаяся стрелочная async функция

Если нам нужен await вне async функции (например, в верхнем уровне модуля), то для этого подойдет самовызывающаяся стрелочная async функция:

(async () => { // start
  const promise = Promise.resolve('abc');
  const value = await promise;
  assert.equal(value, 'abc');
})(); // end
Enter fullscreen mode Exit fullscreen mode

Ее результатом будет промис:

const promise = (async () => 123)();
promise.then(x => assert.equal(x, 123));
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Обе асинхронные функции запустятся в одно время и, когда обе выполнятся, 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'
Enter fullscreen mode Exit fullscreen mode

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(); // ждать
}
Enter fullscreen mode Exit fullscreen mode

В примере выше, мы не ожидаем выполнения метода .write(), потому что не важно, когда это произойдет. Гораздо важнее, когда выполнится .close().

Примечание: каждый вызов метода .write()начинается синхронно, что предотвращает конкуренцию.

38.7.2 Может иметь смысл await и игнорировать результат

Иногда может иметь смысл использование await, даже если мы игнорируем результат. Например:

await longRunningAsyncOperation();
console.log('Done!');
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)