Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.
Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.
Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:
Он, без особых сложностей, выводит в консоль числа от 1 до 3.
Теперь — код асинхронный:
Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции
Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.
Предположим, перед нами стоит задача поиска пользователя GitHub и загрузки данных о его репозиториях. Главная проблема тут в том, что мы не знаем точного имени пользователя, поэтому нам нужно вывести всех пользователей с именами, похожими на то, что мы ищем, и их репозитории.
В плане интерфейса ограничимся чем-нибудь простым.

Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев
В примерах выполнение запросов будет выполнено средствами
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.
С функциями в JS можно делать очень много всего, в том числе — передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:
Используя этот подход для решения нашей задачи, мы можем написать такую функцию
Теперь функция для выполнения запроса принимает параметр
Разберём то, что здесь происходит:
Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.
Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:
Этот подход работает, но используя его, мы рискуем столкнуться с проблемами вроде состояния гонки запросов и сложностей с обработкой ошибок. Однако, основная неприятность, связанная с коллбэками, которых, считая то, что происходит в цикле

Ад коллбэков во всей красе. Изображение взято отсюда.
В данном случае под «состоянием гонки» мы понимаем ситуацию, когда мы не контролируем порядок получения данных о репозиториях пользователей. Мы запрашиваем данные по всем пользователям, и вполне может оказаться так, что ответы на эти запросы окажутся перемешанными. Скажем, ответ по десятому пользователю придёт первым, а по второму — последним. Ниже мы поговорим о возможном решении этой проблемы.
Используя промисы можно улучшить читабельность кода. В результате, например, если в ваш проект придёт новый разработчик, он быстро поймёт, как там всё устроено.
Для того, чтобы создать промис, можно воспользоваться такой конструкцией:
Разберём этот пример:
Вот что стоит помнить, работая с промисами:
Обратите внимание на то, что промисы можно создавать без использования отдельно определённых функций, описывая функции в момент создания промисов. То, что показано в нашем примере — лишь распространённый способ инициализации промисов.
Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.
При таком подходе, когда вы вызываете

Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён
Теперь, воспользовавшись новой функцией
Здесь мы оказываемся в первом выражении
Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:
При таком подходе один взгляд на имена коллбэков в выражениях
На самом деле, это лишь вершина айсберга того, что называется промисами. Вот материал, который я рекомендую почитать тем, кто хочет более основательно погрузиться в эту тему.
Ещё один подход к решению нашей задачи, который, однако, нечасто встретишь — это генераторы. Тема это немного более сложная, чем остальные, поэтому, если вы чувствуете, что вам это изучать пока рано, можете сразу переходить к следующему разделу этого материала.
Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова
Дело тут в том, что генераторы, вместо
Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция
Тут, как обычно, мы используем аргумент
Генератор будет выглядеть так:
Вот что здесь происходит:
Использование этого всего будет выглядеть так:
Здесь мы можем индивидуально обрабатывать список репозиториев каждого пользователя. Для того, чтобы улучшить этот код, можно было бы выделить функции обратного вызова, как мы уже делали выше.
Я неоднозначно отношусь к генераторам. С одной стороны, можно быстро понять, чего ожидать от кода, взглянув на генератор, с другой, выполнение генераторов приводит к проблемам, похожим не те, что возникают в аду коллбэков.
Надо отметить, что генераторы — возможность сравнительно новая, как результат, если вы рассчитываете на использование вашего кода в старых версиях браузеров, код надо обработать транспилятором. Кроме того, генераторы в написании асинхронного кода используют нечасто, поэтому, если вы занимаетесь командной разработкой, учтите, что некоторые программисты могут быть с ними незнакомы.
На тот случай, если вы решили лучше вникнуть в эту тему, вот и вот — отличные материалы о внутреннем устройстве генераторов.
Этот метод похож на смесь генераторов и промисов. Вам нужно лишь указать, с помощью ключевого слова
Как обычно, сначала — простой пример.
Здесь происходит следующее:
Подготовим функцию
Теперь создаём функцию с ключевым словом
Итак, у нас имеется асинхронная функция
Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об
Минус
Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием
Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде
В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.
Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.
Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.
О синхронном и асинхронном коде
Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:
console.log('1') console.log('2') console.log('3')
Он, без особых сложностей, выводит в консоль числа от 1 до 3.
Теперь — код асинхронный:
console.log('1') setTimeout(function afterTwoSeconds() { console.log('2') }, 2000) console.log('3')
Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции
setTimeout. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds.Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.
Постановка задачи
Предположим, перед нами стоит задача поиска пользователя GitHub и загрузки данных о его репозиториях. Главная проблема тут в том, что мы не знаем точного имени пользователя, поэтому нам нужно вывести всех пользователей с именами, похожими на то, что мы ищем, и их репозитории.
В плане интерфейса ограничимся чем-нибудь простым.

Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев
В примерах выполнение запросов будет выполнено средствами
XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:// аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos' function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { // Код обработки успешного завершения запроса } else { // Обрабатываем ответ с сообщением об ошибке } } } xhr.ontimeout = function () { // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию } xhr.open('get', url, true) xhr.send(); }
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.
Функции обратного вызова
С функциями в JS можно делать очень много всего, в том числе — передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:
// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis". doThis(andThenThis) // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию function andThenThis() { console.log('and then this') } // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант function doThis(callback) { console.log('this first') // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится callback() }
Используя этот подход для решения нашей задачи, мы можем написать такую функцию
request:function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log('Timeout') } xhr.open('get', url, true) xhr.send(); }
Теперь функция для выполнения запроса принимает параметр
callback, поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, function handleReposList(err, repos) { if (err) throw err //Здесь обработаем список репозиториев }) }) })
Разберём то, что здесь происходит:
- Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
- После завершения запроса вызывается коллбэк
handleUsersList;
- Если не было ошибок, разбираем ответ сервера c помощью J
SON.parse, преобразовываем его, для удобства, в объект;
- После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что
repos_url— это URL для наших следующих запросов, и получили мы его из первого запроса.
- Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это
handleReposList. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.
Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.
Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:
try { request(userGet, handleUsersList) } catch (e) { console.error('Request boom! ', e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err // Здесь обрабатываем список репозиториев console.log('My very few repos', repos) }
Этот подход работает, но используя его, мы рискуем столкнуться с проблемами вроде состояния гонки запросов и сложностей с обработкой ошибок. Однако, основная неприятность, связанная с коллбэками, которых, считая то, что происходит в цикле
forEach, здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.
Ад коллбэков во всей красе. Изображение взято отсюда.
В данном случае под «состоянием гонки» мы понимаем ситуацию, когда мы не контролируем порядок получения данных о репозиториях пользователей. Мы запрашиваем данные по всем пользователям, и вполне может оказаться так, что ответы на эти запросы окажутся перемешанными. Скажем, ответ по десятому пользователю придёт первым, а по второму — последним. Ниже мы поговорим о возможном решении этой проблемы.
Промисы
Используя промисы можно улучшить читабельность кода. В результате, например, если в ваш проект придёт новый разработчик, он быстро поймёт, как там всё устроено.
Для того, чтобы создать промис, можно воспользоваться такой конструкцией:
const myPromise = new Promise(function(resolve, reject) { // Здесь будет код if (codeIsFine) { resolve('fine') } else { reject('error') } }) myPromise .then(function whenOk(response) { console.log(response) return response }) .catch(function notOk(err) { console.error(err) })
Разберём этот пример:
- Промис инициализируется с помощью функции, в которой есть вызовы методов
resolveиreject;
- Асинхронный код помещают внутри функции, созданной с помощью конструктора
Promise. Если код будет выполнен успешно, вызывают методresolve, если нет —reject;
- Если функция вызовет
resolve, будет исполнен метод.thenдля объектаPromise, аналогично, если будет вызванreject, будет исполнен метод.catch.
Вот что стоит помнить, работая с промисами:
- Методы
resolveиrejectпринимают только один параметр, в результате, например, при выполнении команды видаresolve('yey', 'works'), коллбэку.thenбудет передано лишь'yey';
- Если объединить в цепочку несколько вызовов
.then, в конце соответствующих коллбэков следует всегда использоватьreturn, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
- При выполнении команды
reject, если следующим в цепочке идёт.then, он будет выполнен (вы можете считать.thenвыражением, которое выполняется в любом случае);
- Если в цепочке из вызовов
.thenв каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение.catch;
- У промисов есть три состояния: «pending» — состояние ожидания вызова
resolveилиreject, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовомresolve, и неуспешному, с вызовомreject, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.
Обратите внимание на то, что промисы можно создавать без использования отдельно определённых функций, описывая функции в момент создания промисов. То, что показано в нашем примере — лишь распространённый способ инициализации промисов.
Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.
function request(url) { return new Promise(function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject('timeout') } xhr.open('get', url, true) xhr.send(); }) }
При таком подходе, когда вы вызываете
request, возвращено будет примерно следующее.
Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён
Теперь, воспользовавшись новой функцией
request, перепишем остальной код.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log('will be pending when logged', myPromise) myPromise .then(function handleUsersList(users) { console.log('when resolve is found it comes here with the response, in this case users ', users) const list = JSON.parse(users).items return Promise.all(list.map(function(user) { return request(user.repos_url) })) }) .then(function handleReposList(repos) { console.log('All users repos in an array', repos) }) .catch(function handleErrors(error) { console.log('when a reject is executed it will come here ignoring the then statement ', error) })
Здесь мы оказываемся в первом выражении
.then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch.Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) { return request(user.repos_url) })) } function handleReposList(repos) { console.log('All users repos in an array', repos) } function handleErrors(error) { console.error('Something went wrong ', error) }
При таком подходе один взгляд на имена коллбэков в выражениях
.then раскрывает смысл вызова userRequest. С кодом легко работать, его легко читать.На самом деле, это лишь вершина айсберга того, что называется промисами. Вот материал, который я рекомендую почитать тем, кто хочет более основательно погрузиться в эту тему.
Генераторы
Ещё один подход к решению нашей задачи, который, однако, нечасто встретишь — это генераторы. Тема это немного более сложная, чем остальные, поэтому, если вы чувствуете, что вам это изучать пока рано, можете сразу переходить к следующему разделу этого материала.
Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова
function. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) // выведет 1 console.log(fooIterator.next().value) // выведет 2 fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'
Дело тут в том, что генераторы, вместо
return, используют выражение yield, которое останавливает выполнение функции до следующего вызова .next итератора. Это похоже на выражение .then в промисах, которое выполняется при разрешении промиса.Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция
request:function request(url) { return function(callback) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log('timeout') } xhr.open('get', url, true) xhr.send() } }
Тут, как обычно, мы используем аргумент
url, но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.Генератор будет выглядеть так:
function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i<=users.length; i++) { yield request(users[i].repos_url) } }
Вот что здесь происходит:
- Мы ожидаем подготовки первого запроса, возвращая ссылку на функцию и ожидая коллбэка для этого первого запроса (вспомните функцию
request, которая она принимаетurlи возвращает функцию, которая ожидает коллбэк);
- Ожидаем готовности списка пользователей,
users, для отправки в следующий.next;
- Проходимся по полученному массиву
usersи ожидаем, для каждого из них,.next, возвращая, для каждого, соответствующий коллбэк.
Использование этого всего будет выглядеть так:
try { const iterator = list() iterator.next().value(function handleUsersList(err, users) { if (err) throw err const list = JSON.parse(users).items // Отправляем список пользователей итератору iterator.next(list) list.forEach(function(user) { iterator.next().value(function userRepos(error, repos) { if (error) throw repos // Здесь обрабатываем информацию о репозиториях каждого пользователя console.log(user, JSON.parse(repos)) }) }) }) } catch (e) { console.error(e) }
Здесь мы можем индивидуально обрабатывать список репозиториев каждого пользователя. Для того, чтобы улучшить этот код, можно было бы выделить функции обратного вызова, как мы уже делали выше.
Я неоднозначно отношусь к генераторам. С одной стороны, можно быстро понять, чего ожидать от кода, взглянув на генератор, с другой, выполнение генераторов приводит к проблемам, похожим не те, что возникают в аду коллбэков.
Надо отметить, что генераторы — возможность сравнительно новая, как результат, если вы рассчитываете на использование вашего кода в старых версиях браузеров, код надо обработать транспилятором. Кроме того, генераторы в написании асинхронного кода используют нечасто, поэтому, если вы занимаетесь командной разработкой, учтите, что некоторые программисты могут быть с ними незнакомы.
На тот случай, если вы решили лучше вникнуть в эту тему, вот и вот — отличные материалы о внутреннем устройстве генераторов.
Async/await
Этот метод похож на смесь генераторов и промисов. Вам нужно лишь указать, с помощью ключевого слова
async, какую функцию предполагается выполнять асинхронно, и, используя await, сообщить системе о том, какая часть кода должна ждать разрешения соответствующего промиса.Как обычно, сначала — простой пример.
sumTwentyAfterTwoSeconds(10) .then(result => console.log('after 2 seconds', result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => { setTimeout(() => { resolve(value) }, 2000); }); }
Здесь происходит следующее:
- Имеется асинхронная функция
sumTwentyAfterTwoSeconds;
- Мы предлагаем коду подождать разрешения промиса
afterTwoSeconds, который может завершиться вызовомresolveилиreject;
- Выполнение кода заканчивается в
.then, где завершается операция, отмеченная ключевым словомawait, в данном случае — это всего одна операция.
Подготовим функцию
request к использовании в конструкции async/await:function request(url) { return new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject('timeout') } xhr.open('get', url, true) xhr.send() }) }
Теперь создаём функцию с ключевым словом
async, в которой используем ключевое слово await:async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) { const repos = await request(user.repos_url) handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) // Обрабатываем тут репозитории для каждого пользователя console.log(user, userRepos) }
Итак, у нас имеется асинхронная функция
list, которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach, чтобы сформировать список репозиториев. Вызвать всё это очень просто:list() .catch(e => console.error(e))
Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об
async/await можно почитать здесь.Минус
async/await, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.Итоги
Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием
async/await. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде
$.ajax и fetch. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.
Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

