Pull to refresh

Async/Await в javascript. Взгляд со стороны

Reading time 6 min
Views 136K


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

Первое что хочется развеять, это распространенное заблуждение о том, что async/await — это фича ES7.

По моему мнению, использование терминов ES6 и ES7 само по себе не очень верное и может ввести разработчиков в заблуждение. После удачного релиза спецификации ES2015, называемой ES6, у многих людей сложилось ошибочное мнение, что все в нее не вошло и заполифилено через babel — это фичи ES7. Это не так. Вот список того что появится с релизом спецификации ES2016. Как видите он не такой большой и async/await в нем никак не значится.

Я хочу, чтобы мы говорили правильно. И говоря о той, или иной фиче, ссылались на конкретную спецификацию в рамках которой она описана и реализована, а не мифические ES6, ES7 … ESN.

Двигаемся дальше. Так что же такое async/await простыми словами?


Говоря общедоступным языком async/await — это Promise.

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

Это очень важный момент для понимания, как такие функции работают и чего ожидать при работе с ними.

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


Вот простой пример асинхронного Redux экшена для выхода из кабинета:

export function logout(router) {
  return async (dispatch) => {
    try {
      const {data: {success, message}} = await axios.get('/logout');
 
      (success)
        ? dispatch({ type: LOGOUT_SUCCESS })
        : dispatch({ type: LOGOUT_FAILURE, message });
 
     } catch (e) {
         dispatch({ type: LOGOUT_FAILURE, e.data.message });
     }
   };
}

А теперь идем от общего к частному


После прочтения ряда статей и самостоятельно поигравшись, я составил для себя небольшой бриф, отвечающий на основные вопросы, с небольшими примерами.

Что нужно сделать чтобы начать работу?


Если не использовать никакой системы сборки, то достаточно установить babel и babel-runtime.

babel test.js -o test-compile.js —optional runtime —experimental

В остальных случаях, лучше смотреть настройки исходя их системы сборки и версии babel. Это очень важно, так как настройки в версии babel5 и babel6 сильно различаются.

Как создается асинхронная функция?


async function unicorn() {
  let rainbow = await getRainbow();
  return rainbow.data.colors
}

Создание асинхронной функции состоит из двух основных частей:

1. Использования слова async перед объявлением функции.

Как мы видим из примера c logout(), это так же работает при использовании стрелочных функций. Еще это работает для функций классов и статичных функций. В последнем случае async пишется после static.

2. В теле самой функции мы должны использовать слово await.

Использование слова await сигнализирует о том, что бы основной код ждал и не возвращал ответ, пока не выполниться какое-то действие. Оно просто обрабатывает Promise для нас и ждет пока он вернет resolve или reject. Таким образом, создается впечатление, что код выполняется синхронно.

* Для работы с await функция должна быть асинхронной и объявлена с помощью ключевого слова async. В противном случае это просто не будет работать.

Как работает await и какую функцию выполняет?


Как говорилось ранее, await ожидает любой Promise. Проводя аналогию с работой объекта Promise, можно сказать, что await выполняет точно такую же функцию что и его метод .then(). Единственная существенная разница в том, что она не требует никаких callback функций для получения и обработки результата. Собственно за счет этого и создается впечатление что код выполняется синхронно.

Хорошо, если await это аналог .then() у Promise, как же мне тогда поймать и обработать исключения?


async function unicorn() {
  try {
    let rainbow = await getRainbow();
    return rainbow.data.colors;
  } catch(e) {
    return {
      message: e.data.message,
      somaText: ‘Текст о не легкой жизни единорогов’
    }
  }
}

Так как код в синхронном стиле, по этой причине мы можем использовать старый добрый try/catch для решения подобных задач.

Дополнительно хочется акцентировать на этом внимание.



Использование try/catch это единственный способ поймать и обработать ошибку. Если по каким-то причинам вы решите его не использовать или просто забыли, это может привести к отсутствию возможности обработки, а так же потере вовсе.

В какой момент происходит выполнение кода следующего за await?


async function unicorn() {
  let _colors = [];
  let rainbow = await getRainbow();
  
  if(rainbow.data.colors.length) {
     _colors = rainbow.colors.map((color) => color.toUpperCase());
  }
  
  return _colors;
}

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

Что если функция используемая с await не возвращает Promise?


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

Что если объявить функцию асинхронной, но не использовать await?


async function unicorn() {
  let rainbow = getRainbow();
  return rainbow;
}

В таком случае, на выходе мы получим просто ссылку на Promise функции getRainbow().

Что будет если я напишу несколько функций использующих await подряд?


async function unicorn() {
   let rainbow = await getRainbow();
   let food = await getFood();
   return {rainbow, food}
}

Такой код будет выполняться последовательно. Сначала отработает getRainbow(), после того как она вернет resolve или reject начнет работать getFood(). Один вызов, один результат.

А если мне нужно одновременно получить результат от нескольких вызовов?


async function unicorn() {
  let [rainbow, food] = await Promise.all([getRainbow(), getFood()]);
  return {rainbow, food}
}

Так как мы уже разобрались, что мы имеем дело с Promise. Следовательно можно использовать метод .all() объекта Promise для решения такого рода задач.

Дополнительно хочу заметить, что конструкция await * arrayOfPromises больше не актуальна и удалена из спецификации. При попытке ее использовать вы по получите сообщение с любовью о том, что лучше использовать Promise.all().

Пример сообщения:
  await* has been removed from the async functions proposal. Use Promise.all()

Обновил информацию по конструкции await*. Спасибо xGromMx и degorov.

Что еще хорошо бы знать для успешной работы?


async function getAllUnicorns(names) {
  return await Promise.all(names.map(async function(name) {
    var unicorn = await getUnicorn(name);
    return unicorn;
  }));
}

Надо помнить, что если ты начинаешь использовать async/await в своем проекте, нужно быть готовым к тому, что почти весь твой стек должен будет быть асинхронным. А это может добавить не мало проблем и неудобств.

Вроде бы все.

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

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

Выводы:


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

Но это на первый взгляд.

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

Почти все вечно зеленые браузеры, из коробки, на 93%-98% поддерживают фичи ES2015 (таблица). Для меня это означает, что начиная новый проект, исходя из требований и стека, я уже задумаюсь об необходимости babel на проекте.

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

Так же мне очень не нравится тот факт, что если я забыл применить await или просто не удачный копипаст, вместо автоматического вылета на ошибку, я ничего не получу, кроме ссылки на Promise. Это может быть черевато последствиями, особенно когда большой проект с несколькими разработчиками.

И последнее.

Большинство задач с использованием async/await прекрасно решаются с помощью генераторов.

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

Поддержка в NodeJS


Async/await уже экспериментально попал в V8. Это значит что с версии nodejs 7 можно с ним поиграться и поработать прямо из коробки.
Как это сделать:
NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly  
nvm install 7  
nvm use 7

node --harmony-async-await app.js  


Итого


Отвечаю себе на вопрос заданный в самом начале:
Если я и буду использовать асинхронные функции, то только на своих Pet проектах и не очень больших рабочих, в основном для написания API. По крайней мере пока все не стандартизируется и не будет под флагом -экспериментально.

Например мне понравилось использовать их в экшенах для Redux. Выглядит все красиво и гармонично.

Этот материал я писал в первую очередь для себя, чтобы разобраться с интересующим меня вопросом. Если данный материал будет еще кому-то полезен, я буду очень рад.

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

Всем спасибо за внимание. Удачи!
Tags:
Hubs:
+31
Comments 53
Comments Comments 53

Articles