Асинхронные функции: делаем обещания дружелюбными

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

Асинхронные функции включены по умолчанию в Chrome, Edge, Firefox и Safari, и они, откровенно говоря, великолепны. Они позволяют писать код на основе промисов так, как если бы он был синхронным, но без блокировки основного потока. Они делают ваш асинхронный код менее «умным» и более читабельным.

Асинхронные функции работают следующим образом:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

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

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

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

  • Хром: 55.
  • Край: 15.
  • Фаерфокс: 52.
  • Сафари: 10.1.

Источник

Пример: регистрация выборки

Допустим, вы хотите получить URL-адрес и записать ответ в виде текста. Вот как это выглядит с использованием обещаний:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

И вот то же самое с использованием асинхронных функций:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Это то же количество строк, но все обратные вызовы исчезли. Это облегчает чтение, особенно для тех, кто менее знаком с обещаниями.

Асинхронные возвращаемые значения

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

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

…вызов hello() возвращает обещание, которое выполняется с помощью "world" .

async function foo() {
  await wait(500);
  throw Error('bar');
}

…вызов foo() возвращает обещание, которое отклоняется с помощью Error('bar') .

Пример: потоковая передача ответа

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

Вот оно с обещаниями:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Взгляни на меня, Джейк «обещающий» Арчибальд. Видите, как я вызываю processResult() внутри себя, чтобы настроить асинхронный цикл? Написав это, я почувствовал себя очень умным . Но, как и в случае с большинством «умных» кодов, вам придется смотреть на него целую вечность, чтобы понять, что он делает, как на одну из тех картинок волшебного глаза из 90-х.

Давайте попробуем это еще раз с асинхронными функциями:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

Все "умное" пропало. Асинхронный цикл, из-за которого я чувствовал себя таким самодовольным, заменен надежным, скучным циклом while. Гораздо лучше. В будущем вы получите асинхронные итераторы , которые заменят цикл while на цикл for-of , что сделает его еще более аккуратным.

Другой синтаксис асинхронной функции

Я уже показывал вам async function() {} , но ключевое слово async можно использовать с другим синтаксисом функции:

Стрелочные функции

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Методы объекта

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Методы класса

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

Осторожный! Избегайте слишком последовательного подхода

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

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Вышеупомянутое занимает 1000 мс, тогда как:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

Вышеописанное занимает 500 мс, поскольку оба ожидания происходят одновременно. Давайте рассмотрим практический пример.

Пример: вывод выборок по порядку

Допустим, вы хотите получить серию URL-адресов и записать их как можно скорее в правильном порядке.

Глубокий вдох — вот как это выглядит с обещаниями:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Да, верно, я использую reduce , чтобы связать последовательность обещаний. Я такой умный . Но это немного умного кодирования, без которого вам лучше обойтись.

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

Не рекомендуется — слишком последовательно
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Выглядит намного аккуратнее, но моя вторая выборка не начнется, пока моя первая выборка не будет полностью прочитана, и так далее. Это намного медленнее, чем пример с обещаниями, в котором выборка выполняется параллельно. К счастью, есть идеальная золотая середина.
Рекомендуется - красиво и параллельно
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
В этом примере URL-адреса извлекаются и считываются параллельно, но «умный» бит reduce заменяется стандартным, скучным и читаемым циклом for.

Обходной путь поддержки браузера: генераторы

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

Babel сделает это за вас, вот пример с помощью Babel REPL.

  • обратите внимание, насколько похож транспилированный код. Это преобразование является частью пресета Babel es2017 .

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

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…вы должны включить полифилл и написать:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Обратите внимание, что вам нужно передать генератор ( function* ) в createAsyncFunction и использовать yield вместо await . В остальном он работает так же.

Решение: регенератор

Если вы ориентируетесь на старые браузеры, Babel также может транспилировать генераторы, позволяя вам использовать асинхронные функции вплоть до IE8. Для этого вам понадобится пресет Babel es2017 и пресет es2015 .

Результат не такой красивый , поэтому следите за раздуванием кода.

Асинхронизация всего!

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

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