Используй Async/Await в JavaScript, как профессионал

В жизни каждого программиста наступает такой момент, когда нужно разобраться с тем, как работает асинхронный код.

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

Что такое «асинхронный код»?

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

console.log('hi')
console.log('my name')
console.log('is')
console.log('jeff!')

// Вывод: 
// hi
// my name
// is
// jeff!

Здесь код выполняется в том порядке, какой и ожидается. Такой код называется синхронным.

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

console.log('hi')
console.log('my name')

setTimeout(() => {
 console.log('is')
},500)

console.log('jeff!')

// Вывод:
// hi
// my name
// jeff!
// is

Что произошло с выводом?

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

А произошло вот что: функция времени ожидания timeout была вызвана одновременно с выполнением остальной части кода, но запуск внутреннего log произошел спустя 500 мс после этого. Отсюда и выдача неверного вывода.

Строка кода выполнилась в другое время, нежели остальные. Это и называется асинхронностью.

И как же решить эту проблему?

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

Промисы

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

Разница здесь в том, что после завершения асинхронного кода промис возвращает состояние, позволяющее выполнять дальнейшие действия с помощью таких методов, как then() и catch().

Примечание: в этой статье не будем слишком подробно разбирать промисы, нам важно иметь общее представление о них.

Рассмотрим следующий пример. Вывод снова в норме:

const async_func = () => {
 return new Promise((resolve,reject) => {
  setTimeout(() => {  
     resolve('is')
   },500)
 })
}

console.log('hi')
console.log('my name')

async_func().then(res => {
 console.log(res);
 console.log('jeff!');
}).catch(error => {
 console.error(error);
})

// Вывод:
// hi
// my name
// is
// jeff!

Но как это получилось?

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

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

А что будет с выводом, если вместо resolve промис вернет reject?

const async_func = () => {
 return new Promise((resolve,reject) => {
  setTimeout(() => {  
     reject(false)
   },500)
 })
}

console.log('hi')
console.log('my name')

async_func().then(res => {
 console.log(res);
 console.log('jeff!');
}).catch(error => {
 console.error(error);
})

// Вывод:
// hi
// my name

Когда промис возвращает reject, вызывается метод catch(). В нашем случае этот метод регистрирует сообщение об ошибке и завершает остальную часть кода. Когда это происходит, регистрировать ошибку не нужно.

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

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

Но есть ли более лучшее решение?

Вводим async/await

Используя ключевые слова async/await в JavaScript, мы делаем код гораздо более отточенным и удобным для восприятия. Посмотрим, что произойдет с кодом при задействовании async/await в предыдущем примере:

const async_func = () => {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
         resolve('is');
       },530)
     });
}

const main = async () => {
  console.log('hi')
  console.log('my name')
  const result = await async_func();
  console.log(result);
  console.log('jeff!')
}

main();

// Вывод:
// hi
// my name
// is
// jeff!

Теперь код выглядит намного лучше!

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

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

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

Мы также обернули остальную часть синхронного кода в функцию main() и задействовали ключевое слово async. Не у одних промисов есть правила  —  чтобы все это работало, нужно соблюдать правила async/await.

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

Предположим, теперь нам надо добавить в пример еще одну часть асинхронного кода. Что будет в этом случае?

const async_func = () => {
  return new Promise((resolve,reject) => {
   setTimeout(() => {
      resolve('is');
    },530)
  });
} 
  
 const async_func2 = () => {
 	return new Promise((resolve,reject) => {
   setTimeout(() => {
      resolve('and I love 2 code');
    },500)
  });
 } 

const main = async () => {
  console.log('hi')
  console.log('my name')
  const result = await async_func();
  console.log(result);
  console.log('jeff!')
  const result2 = await async_func2();
  console.log(result2)
}

main();

// Вывод:
// hi
// my name
// is
// jeff!
// and I love 2 code

Обратите внимание: здесь мы добавляем еще один кусок асинхронного кода и вывод остается в том же порядке, какой ожидался.

Посмотрите: для второй асинхронной функции выполнение задано на 30 мс раньше первой. Так почему же выполнение не произошло в таком порядке?

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

Но при задействовании async/await код останавливается на первой функции и ждет 530 мс до возвращения resolve. Затем добирается до второй функции и ждет 500 мс также до возвращения resolve. В этом и заключается мощь async/await.

Рассмотрим более практический пример с использованием async/await:

const getCharacters = () => {
	return new Promise((resolve,reject) => {
  	setTimeout(() => {
    	const characters = [
		'rick',
		'jerry',
		'morty',
		'rick',
		'kate',
		'summer',
		'summer',
		'kate',
		'noob noob',
		'jerry',
		'morty jr',
		'morty',
		'evil morty',
		'evil rick',
		'noob noob',
		'morty jr'
	]
      resolve(characters);
    },300)
  })
}

const main = async () => {
	const characters = await getCharacters();
  const counts = characters.reduce((accumulator,character,index) => {
  	accumulator[character] ? accumulator[character].count++ : 
	accumulator[character] = {name: character, count: 1, index: index};
    return accumulator;
  },{})
  
  const imposters = Object.entries(counts)
  .filter(([key,obj]) => obj.count === 1)
  .map(([key,obj]) => ({name: key, index: obj.index}))
  
  console.log(imposters);
}

main();

// Вывод:
// [{name: 'evil morty', index: 12},{name: 'evil rick', index: 13}]

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

Задействовав await, мы ждем в функции main возвращения промисом resolve, а в ответ получаем запрошенный список персонажей.

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

После этого выполняем фильтрацию объекта counts для любых персонажей, счетчик которых равен единице, а затем сопоставляем с массивом imposters.

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

Если бы мы в этом практическом примере обошлись без async/await, то имели бы дело с ошибками в коде.

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

Заключение

Ну вот и все. Понять, как работать с асинхронным кодом, бывает непросто, особенно когда имеешь с ним дело впервые. Чем больше сталкиваешься с асинхронным кодом, тем лучше с такими ситуациями справляешься.

Надеюсь, вы получили некоторое представление о том, как работает async/await и очень скоро станете еще большим профессионалом в JavaScript.

Спасибо за внимание🙂.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Michael Miller: How to Use Async/Await in JavaScript Like a Pro

Предыдущая статья19 лучших инструментов прототипирования для дизайнеров UX/UI
Следующая статьяКонкурентность на Go: объяснение шаблона Worker Pool