Асинхронность в JS
Чтобы понять, что такое асинхронность, сперва поговорим о синхронном коде и том, как в принципе JavaScript выполняет код.
Чтобы выполнить код, нам нужен JavaScript Engine (движок) — программа, которая «читает и выполняет» то, что мы написали. Самый распространённый движок среди всех — это V8, он используется в Google Chrome и Node.js.
Выполнение JS-кода — однопоточное. Это значит, что в конкретный момент времени движок может выполнять не более одной строки кода. То есть вторая строка не будет выполнена, пока не выполнится первая.
Такое выполнение кода (строка за строкой) называется синхронным.
Синхронный код и его проблемы
Скопировать ссылку «Синхронный код и его проблемы» Скопировано
Синхронный код понятный, его удобно читать, потому что он выполняется ровно так, как написан:
console.log('A')console.log('B')console.log('C')console.log('A') console.log('B') console.log('C')
A B C
Никаких сюрпризов: в каком порядке команды указаны — в таком они и выполнились.
Однако с ним могут возникать некоторые проблемы. Представим, что нам нужно выполнить какую-то операцию, требующую некоторого времени — например, напечатать в консоли приветствие, но не сразу, а через 5 секунд. Ниже псевдокод — синхронная функция задержки delay ( ) вымышленная:
function greet() console.log('Hello!')> delay(5000)greet()function greet() console.log('Hello!') > delay(5000) greet()
Через 5 секунд бездействия вывелось бы:
Hello!
И всё вроде хорошо, приветствие бы действительно напечаталось спустя 5 секунд, однако проблема здесь в другом.
Если бы мы запустили синхронную функцию задержки delay ( ) , то движок бы ничем другим заниматься в это время не мог.
Мы помним, что выполнение синхронного кода — строка за строкой. То есть пока delay ( ) не выполнится до конца, к следующей строке интерпретатор не перейдёт.
А это значит, что пока не пройдёт 5 секунд, и delay ( ) не выполнится, мы вообще ничего сделать не сможем: ни вывести что-то в консоль ещё, ни выполнить другие функции, в особо тяжёлых случаях — даже передвинуть курсор.
Такие операции, которые не дают выполнять ничего кроме них самих, пока они не завершатся, называются блокирующими выполнение.
Асинхронный код
Скопировать ссылку «Асинхронный код» Скопировано
Теперь попробуем решить эту же задачу, но так, чтобы наш код не блокировал выполнение. Для этого мы воспользуемся функцией set Timeout ( ) :
setTimeout(function greet() console.log('Hello!')>, 5000)setTimeout(function greet() console.log('Hello!') >, 5000)
5 секунд молчания, и выведется «Hello!»
Задача решена. В этот раз, однако, в эти «5 секунд молчания» мы можем выполнять другие действия.
setTimeout(function greet() console.log('Hello!')>, 5000) console.log("I'm being called before greet function.")setTimeout(function greet() console.log('Hello!') >, 5000) console.log("I'm being called before greet function.")
Сначала выведется: «I’m being called before greet function», а через 5 секунд — «Hello!»
Возникает несколько вопросов:
- Почему вторая строка кода выполнилась до первой, если JS однопоточный?
- Куда девается set Timeout ( ) на время, пока выполняется другой код?
- Как движок понимает, что пора выводить Hello! ?
Чтобы с этим разобраться, нам надо понять, как функции вызываются «под капотом».
Стек вызовов
Скопировать ссылку «Стек вызовов» Скопировано
При вызове какой-то функции она попадает в так называемый стек вызовов.
Стек — это структура данных, в которой элементы упорядочены так, что последний элемент, который попадает в стек, выходит из него первым (LIFO: last in, first out). Стек похож на стопку книг: та книга, которую мы кладём последней, находится сверху.
В стеке вызовов хранятся функции, до которых дошёл интерпретатор, и которые надо выполнить.
function outer() function inner() // Функция 3 console.log('Hello!') > // Функция 2 inner()> // Функция 1outer()function outer() function inner() // Функция 3 console.log('Hello!') > // Функция 2 inner() > // Функция 1 outer()
Вызываем функцию 1 — outer ( ) , она попадает в стек:
outer;outer;
Вызываем функцию 2 — inner ( ) , теперь в стеке 2 функции, потому что первая ещё не выполнилась до конца:
inner;outer;inner; outer;
Вызываем console . log ( ) , теперь в стеке 3 функции:
console.log;inner;outer;console.log; inner; outer;
Как только console . log ( ) выполнится, она уйдёт из стека, там останется 2 функции:
inner;outer;inner; outer;
Выполнившись, функция inner ( ) тоже уйдёт из стека, в нём останется лишь одна:
outer;outer;
После выполнения всего блока стек станет пустым.
В синхронном коде в стеке хранится вся цепочка вызовов. Поэтому, например, рекурсия без базового случая может приводить к переполнению стека — в нём скапливается слишком большое количество вызовов.
Теперь посмотрим, как ведёт себя стек вызовов при работе с асинхронным кодом:
function main() setTimeout(function greet() console.log('Hello!') >, 2000) console.log('Bye!')> main()function main() setTimeout(function greet() console.log('Hello!') >, 2000) console.log('Bye!') > main()
Вызываем функцию main ( ) . Стек:
main();main();
Вызываем set Timeout ( ) . Стек:
setTimeout();main();setTimeout(); main();
set Timeout завершился, он выходит из стека:
main();main();
Вызываем console . log ( ‘ Bye!’ ) :
console.log('Bye!');main();console.log('Bye!'); main();
Его вызов завершён, он выходит из стека:
main();main();
Вызов main ( ) тоже завершён, стек становится пуст.
Проходит около 2 секунд, вызывается функция greet ( ) , она попадает в стек:
greet();greet();
Она вызывает console . log ( ‘ Hello!’ ) :
console.log('Hello!');greet();console.log('Hello!'); greet();
Отработав, она уходит из стека:
greet();greet();
После выполнения всего блока стек снова становится пустым.
Первое, что бросается в глаза — set Timeout ( ) завершается сразу, хотя колбэк внутри него ещё не отработал, более того, он даже ещё не был вызван! Здесь нам понадобится ещё одно понятие — цикл событий.
Цикл событий
Скопировать ссылку «Цикл событий» Скопировано
Сперва откроем страшную правду, set Timeout ( ) — это не JavaScript!
Ну. не совсем так, конечно. Функция set Timeout ( ) не является частью JavaScript-движка, это по сути Web API, включённое в среду браузера как дополнительная функциональность.
Эта дополнительная функциональность (Web API) берёт на себя работу с таймерами, интервалами, обработчиками событий. То есть когда мы регистрируем обработчик клика на кнопку — он попадает в окружение Web API. Именно оно знает, когда обработчик нужно вызвать.
Управление тем, как должны вызываться функции Web API, берёт на себя цикл событий (Event loop).
Цикл событий отвечает за выполнение кода, сбор и обработку событий и выполнение подзадач из очереди.
Именно цикл событий ответственен за то, что set Timeout ( ) пропал из стека в прошлом примере. Чтобы увидеть картину целиком, давайте включим в нашу схему все недостающие части. Возьмём тот же самый пример:
function main() setTimeout(function greet() console.log('Hello!') >, 2000) console.log('Bye!')> main()function main() setTimeout(function greet() console.log('Hello!') >, 2000) console.log('Bye!') > main()
Но теперь у нас будет не только стек вызовов — также мы включим Web API и очередь задач, которую Web API использует для хранения того, что нужно выполнить.
| Стек | Web API | Очередь задач |
|---|---|---|
| main ( ) |
В Web API и очереди задач пока пусто.
Вызов set Timeout ( ) :
| Стек | Web API | Очередь задач |
|---|---|---|
| set Timeout ( ) | ||
| main ( ) |
Когда set Timeout ( ) исчезает из стека, он попадает в видимость Web API, где интерпретатор понимает, что внутри него есть функция greet ( ) , которую надо выполнить через 2 секунды:
| Стек | Web API | Очередь задач |
|---|---|---|
| main ( ) | set Timeout ( greet ) |
После этого выполняется вызов консоли console . log ( ‘ Bye!’ ) . В Web API находится функция set Timeout ( greet ) . Она будет там до тех пор, пока не пройдёт 2 секунды:
| Стек | Web API | Очередь задач |
|---|---|---|
| console . log ( ‘ Bye!’ ) | set Timeout ( greet ) | |
| main ( ) |
Отработал console . log ( ) , заканчивается работа main ( ) :
| Стек | Web API | Очередь задач |
|---|---|---|
| main ( ) | set Timeout ( greet ) |
main ( ) отработал, стек пуст. 2 секунды ещё не прошло, поэтому set Timeout ( greet ) все ещё в Web API:
| Стек | Web API | Очередь задач |
|---|---|---|
| set Timeout ( greet ) |
Наконец, 2 секунды прошли — функция greet ( ) перемещается в очередь задач:
| Стек | Web API | Очередь задач |
|---|---|---|
| greet ( ) |
Теперь цикл событий перемещает функцию greet ( ) из списка задач в вызов:
| Стек | Web API | Очередь задач |
|---|---|---|
| greet ( ) |
Затем вызов console . log ( ‘ Hello!’ ) :
| Стек | Web API | Очередь задач |
|---|---|---|
| console . log ( ‘ Hello!’ ) | ||
| greet ( ) |
И наконец стек пуст.
Заметьте, что стек вызовов и очередь задач называются именно стеком и очередью. Потому что вызовы из стека работают по принципу «последний зашёл, первый вышел» (LIFO: last in, first out), а в очереди — по принципу «первый зашёл, первый вышел» (FIFO: first in, first out).
Очередь — структура данных, в которой элементы упорядочены так, что первый попавший в очередь элемент покидает её первым.
Таким образом цикл событий работает с асинхронным кодом — то есть таким, который выполняется не построчно.
Очень хорошо работу цикла событий иллюстрирует инструмент Loupe Филипа Робертса, а также его доклад «What the heck is the event loop anyway?».
Loupe интерактивный, попробуйте ввести какой-нибудь код в поле слева, и справа будет показываться, что и в какой момент попадает в стек вызовов и очередь событий:
Колбэки
Скопировать ссылку «Колбэки» Скопировано
Пример с set Timeout ( ) , который мы рассмотрели, показывает, как работают функции обратного вызова — колбэки.
Callback (колбэк, функция обратного вызова) — функция, которая вызывается в ответ на совершение некоторого события.
В нашем случае таким событием было срабатывание таймера через 2 секунды, а колбэком — функция greet ( ) . В целом, событием может быть что угодно:
- ответ от сервера;
- завершение какой-то длительной вычислительной задачи;
- получение доступа к каким-то API устройства, на котором выполняется код.
Таким образом колбэк — это первый способ обработать какое-либо асинхронное действие.
Изначально колбэки были единственным способом работать с асинхронным кодом в JavaScript. Большая часть асинхронного API Node.js была написана именно на колбэках и создана для использования с колбэками.
Это, в принципе, логично — ментальная модель достаточно простая: «выполни эту функцию, когда случится это событие».
Однако у колбэков есть неприятный минус, так называемый ад колбэков (callback hell).
Ад колбэков (Callback-hell)
Скопировать ссылку «Ад колбэков (Callback-hell)» Скопировано
Нагляднее всего его можно показать на примере.
Допустим, у нас есть ряд асинхронных задач, которые зависят друг от друга: то есть первая задача запускает по завершении вторую, вторая — третью и т. д.
setTimeout(() => setTimeout(() => setTimeout(() => setTimeout(() => console.log('Hello!') >, 5000) >, 5000) >, 5000)>, 5000)setTimeout(() => setTimeout(() => setTimeout(() => setTimeout(() => console.log('Hello!') >, 5000) >, 5000) >, 5000) >, 5000)
Если одна задача запускает другую, та — третью, и так далее, мы можем получить вот такую «башню» из обратных вызовов.
И такая башня может получиться где угодно. Если мы делаем несколько последовательных запросов к серверу, зависящих друг от друга, то это может выглядеть вот так:
function request(url, onSuccess) /*. */> request('/api/users/1', function (user) request(`/api/photos/$/`, function (photo) request(`/api/crop/$/`, function (response) console.log(response) >) >)>)function request(url, onSuccess) /*. */ > request('/api/users/1', function (user) request(`/api/photos/$user.id>/`, function (photo) request(`/api/crop/$photo.id>/`, function (response) console.log(response) >) >) >)
Читать такое сложно, не говоря уже о тестировании, которое здесь становится очень накладным.
Решить эту проблему были призваны Промисы (Promise).
Промисы (Promise)
Скопировать ссылку «Промисы (Promise)» Скопировано
Промис — это объект-обёртка для асинхронного кода. Он содержит в себе состояние: вначале pending («ожидание»), затем — одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
В понятиях цикла событий промис работает так же, как колбэк: функция, которая должна выполниться ( resolve или reject ), находится в окружении Web API, а при наступлении события — попадает в очередь задач, откуда потом — в стек вызова.
В асинхронных задачах есть разделение между макрозадачами и микрозадачами. Колбэки в промисах попадают в очередь микрозадач, тогда как колбэк в set Timeout ( ) — в очередь макрозадач. Но здесь и сейчас мы в такие детали уходить не будем.
Промисы придумали, чтобы организовывать асинхронный код последовательно.
Та же последовательность запросов из прошлого примера, но переписанная с использованием промисов.
function request(url) return new Promise(function (resolve, reject) let responseFromServer /*. */ resolve(responseFromServer) >)> request('/api/users/1') .then((user) => request(`/api/photos/$/`)) .then((photo) => request(`/api/crop/$/`)) .then((response) => console.log(response))function request(url) return new Promise(function (resolve, reject) let responseFromServer /*. */ resolve(responseFromServer) >) > request('/api/users/1') .then((user) => request(`/api/photos/$user.id>/`)) .then((photo) => request(`/api/crop/$photo.id>/`)) .then((response) => console.log(response))
Код избавился от лишней вложенности, стал плоским и более тестируемым.
Дополнительным плюсом стала возможность обрабатывать ошибки от цепочки промисов в одном месте — последним catch :
request('/api/users/1') .then((user) => request(`/api/photos/$/`)) .then((photo) => request(`/api/crop/$/`)) .then((response) => console.log(response)) .catch((error) => console.error(error))request('/api/users/1') .then((user) => request(`/api/photos/$user.id>/`)) .then((photo) => request(`/api/crop/$photo.id>/`)) .then((response) => console.log(response)) .catch((error) => console.error(error))
Если что-то пошло не так, то программа не упадёт, а управление перейдёт к последней строчке с catch ( ) , причём независимо от того, в каком из запросов ошибка появится.
Также из then ( ) можно вернуть не промис, а обычное значение. Оно обернётся в промис самостоятельно и прокинется в следующий then ( ) :
request('/api/users/1') .then((user) => user.id) .then((userId) => request(`/api/photos/$/`)) .then((photo) => request(`/api/crop/$/`)) .then((response) => console.log(response)) .catch((error) => console.error(error))request('/api/users/1') .then((user) => user.id) .then((userId) => request(`/api/photos/$userId>/`)) .then((photo) => request(`/api/crop/$photo.id>/`)) .then((response) => console.log(response)) .catch((error) => console.error(error))
Промисы оказались удобными, и появился даже такой термин как «промисификация» — когда асинхронную функциональность на колбэках превращали в промисы.
Однако промисы — это тоже не серебряная пуля. У них есть несколько недостатков:
- Код не такой лаконичный, как мог быть.
- В цепочке промисов, как на примере (со стрелочными функциями), невозможно выставить брейкпоинт, потому что нет тела функции. Приходится раскрывать функцию.
- Стек ошибок может содержать в себе then . then . then . then . . . .
- Вложенные условия сильно увеличивают количество кода и ухудшают читаемость.
Для решения этих проблем придумали асинхронные функции.
Асинхронные функции
Скопировать ссылку «Асинхронные функции» Скопировано
Если коротко, асинхронные функции — функции, которые возвращают промисы.
Асинхронная функция помечается специальным ключевым словом async :
async function request() <>const req = async () => <> class SomeClass async request() <>>async function request() > const req = async () => > class SomeClass async request() > >
Они всегда возвращают Промис. Даже если мы явно этого не указывали, как в примерах выше, при вызове они всё равно вернут промис.
async function request() <> // Сработает:request().then(() => <>)async function request() > // Сработает: request().then(() => >)
Однако с асинхронными функциями можно не обращаться с then ( ) — есть более изящное решение.
Связка async / await
Скопировать ссылку «Связка async/await» Скопировано
Внутри асинхронных функций можно вызывать другие асинхронные функции, без каких-либо then ( ) или колбэков, с помощью ключевого слова await .
async function loadPosts() const response = await fetch('/api/posts/') const data = await response.json() return data>async function loadPosts() const response = await fetch('/api/posts/') const data = await response.json() return data >
В примере выше мы используем метод fetch ( ) внутри функции load Posts ( ) .
Все асинхронные функции внутри мы вызываем с await — таким образом промис, который функция возвращает, автоматически разворачивается, и мы получаем значение, которое внутри промиса было.
Плюсы async / await
Скопировать ссылку «Плюсы async/await» Скопировано
Код чище и короче. У нас больше нет цепочек из then ( ) , вместо этого мы получаем плоскую структуру, которая по виду похожа на синхронный код.
Условия и вложенные конструкции становятся чище и проще читаются.
Мы можем обрабатывать ошибки с try — catch . Как и с синхронным кодом, обработка ошибок сводится к оборачиванию опасных операций в try — catch :
async function loadPosts() try const response = await fetch('/api/posts/') const data = await response.json() return data > catch (e) console.log(e) >>async function loadPosts() try const response = await fetch('/api/posts/') const data = await response.json() return data > catch (e) console.log(e) > >
При этом в отличие от .catch ( ) промисов, try-catch поймает не только ошибки, которые были внутри асинхронных функций, но также и ошибки, которые возникли во время обычных синхронных операций.
Можно ставить брейкпоинты (точки останова). Для отладки мы можем поставить брейкпоинт куда угодно, он сработает.
На практике
Скопировать ссылку «На практике» Скопировано
Саша Беспоясов советует
Скопировать ссылку «Саша Беспоясов советует» Скопировано
️ «Отложить выполнение»
Скопировать ссылку «️ «Отложить выполнение»» Скопировано
Иногда нам бывает нужно отложить выполнение функции на потом, но не через какой-то промежуток времени, а чтобы она выполнилась сразу после каких-то операций. Для этого можно использовать set Timeout ( ) с нулевым таймером.
Мы помним, что колбэк из set Timeout ( ) откладывается в очередь задач. Если мы поставим интервал 0 миллисекунд, то эта задача выполнится ровно через один цикл событий — то есть сразу после синхронного кода.
Абсолютное временное значение одного цикла событий может варьироваться от 4 до 100 миллисекунд.
В Node.js и в некоторых браузерах есть set Immediate ( ) , который делает почти то же, что и set Timeout ( ) с нулевым таймером.
️ «Дождаться всех» или «Дождаться первого»
Скопировать ссылку «️ «Дождаться всех» или «Дождаться первого»» Скопировано
Когда мы работаем с запросами к серверу, нам не всегда бывает нужно вызывать запросы последовательно друг за другом.
Бывают ситуации, когда мы хотим:
- либо подождать, когда выполнятся все запросы, и потом сделать что-то;
- либо дождаться выполнения первого запроса из списка и сделать что-то сразу после этого.
Для этого мы можем использовать Promise . all ( ) и Promise . race ( ) .
Когда мы хотим дождаться выполнения всех запросов и сделать что-то после этого:
const request1 = fetch('/api/users')const request2 = fetch('/api/posts')const request3 = fetch('/api/comments') Promise.all([request1, request2, request3]).then((values) => console.log('Загрузились все данные!') console.log(values)>)const request1 = fetch('/api/users') const request2 = fetch('/api/posts') const request3 = fetch('/api/comments') Promise.all([request1, request2, request3]).then((values) => console.log('Загрузились все данные!') console.log(values) >)
Загрузились все данные!
В переменной values будет массив со значениями каждого из промисов, порядок значений в нём будет соответствовать порядку запросов:
[ [user1, user2], [post1, post2], [comment1, comment2]][ [user1, user2], [post1, post2], [comment1, comment2] ]
Когда нам важно, чтобы выполнился хотя бы один:
const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 500, 'First')>) const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'Second')>) Promise.race([promise1, promise2]).then((value) => console.log(value)>) // Secondconst promise1 = new Promise((resolve, reject) => setTimeout(resolve, 500, 'First') >) const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'Second') >) Promise.race([promise1, promise2]).then((value) => console.log(value) >) // Second
️ «Отдельный поток»
Скопировать ссылку «️ «Отдельный поток»» Скопировано
В браузерном JavaScript есть некое подобие многопоточности. Мы можем выносить тяжёлые операции в Web Worker.
Не следует путать Web Worker и Service Worker — это разные технологии.
️ «Асинхронные циклы»
Скопировать ссылку «️ «Асинхронные циклы»» Скопировано
Просто использовать цикл for или метод for Each с асинхронными операциями мы не можем. И цикл for и метод for Each ожидают синхронный код.
Однако мы можем использовать for await . . . of , который появился в ES2018, для обхода асинхронных итерируемых сущностей.
Простой генератор создаёт итерируемую сущность, которую можно «перебрать» через for . . . of :
const urls = ['/api/users', '/api/posts', '/api/comments'] function* requestGenerator() for (const url of urls) yield url >> for (const item of requestGenerator()) console.log(item)>const urls = ['/api/users', '/api/posts', '/api/comments'] function* requestGenerator() for (const url of urls) yield url > > for (const item of requestGenerator()) console.log(item) >
Выведет каждый URL по очереди. Порядок гарантируется — так как код синхронный.
Асинхронный же генератор почти не отличается от обычного, только вместо значений он выбрасывает промисы. И итерировать его придётся через for await . . . of :
async function* removeDataGenerator() for (const url of urls) const response = await fetch(url) const data = await response.json() yield data >> ;(async () => for await (const item of removeDataGenerator()) console.log(item) >>)()async function* removeDataGenerator() for (const url of urls) const response = await fetch(url) const data = await response.json() yield data > > ;(async () => for await (const item of removeDataGenerator()) console.log(item) > >)()
Выведет данные, которые получит от сервера. Порядок гарантируется, потому что следующий запрос не начнёт выполняться пока не будет закончен предыдущий.
Асинхронность в JS: как выполнять долгие сетевые запросы без блокирования основного потока

Асинхронность – это способ координации поведения программы на протяжении какого-то временного отрезка. Разбираем, как в синхронном JavaScript вынести операции за рамки единого потока, чтобы не блокировать действие кода после тяжёлых операций. JavaScript – это однопоточный язык программирования. JS исполняет одну строку кода в одну единицу времени и не предполагает мультитаскинга. Это отличает JS от многопоточного Java, например, где можно создавать отдельные потоки и переносить новый код в отдельно созданный поток.
В JS при выполнении какой-то одной тяжелой операции, весь код, который следует после неё, может заморозиться. То есть скрипт блокируется, и страница, на которой в данный момент времени этот скрипт используется, становится неотзывчивой.
Поскольку все операции проходят в одном потоке, ряд операций можно сделать асинхронными и вынести за рамки единого потока, чтобы не блокировать дальнейшее исполнение кода. Для этого могут использоваться коллбэки, промисы и асинхронные функции, но в современном JavaScript на практике используются только промисы и асинхронные функции.
Рантайм JS в браузере: event loop
Прежде чем разбирать структуру промисов, освежим в памяти процесс выполнения JS-кода в браузере. Чтобы код выполнялся, необходимо наличие ряда компонентов (движок, Web API, очереди и цикл событий или event loop). Рантайм – это по своей сути контейнер, который содержит все эти компоненты.

Web API хранит в себе асинхронные операции и не является частью движка JS, но предоставляет окружение, набор API, предоставляемых движку JS для его взаимодействия с вебом. На схеме для примера изображены 4 сегмента API, но на деле их может быть неограниченное множество. Очередь состоит из коллбэков – операций, которые были отложены по времени ввиду асинхронного поведения JS. Движок – сердце JS, без которого исполнение кода невозможно. Куча (heap) содержит в себе ссылочные элементы. На взаимодействие с ними тратится больше ресурсов, чем на те, что в стеке, в котором содержатся статичные элементы. Стек помимо хранения примитивных данных выполняет поступающие к нему инструкции. Действует по принципу last in – first out. То есть когда приходит какой-то элемент, он накладывается сверху и будет исполнен в самую первую очередь. Цикл событий (event loop) играет роль дирижёра, который делает корректным выполнение программы. Когда стек пустой, ивент луп прокручивается, смотрит в очередь, обнаруживает, есть ли там колбэк, который можно было бы прокинуть в пустой стек. Если есть, то делает это.
Микротаски и макротаски

Существуют две очереди асинхронных операций: очереди микро- и макротасок. Микротаски имеют более высокий приоритет исполнения. Поэтому когда event loop прокручивается, ему не важно, сколько в очереди макротасок – он возьмёт микротаску, даже если она в очереди единственная, а макротасок 500. Можно сказать, что у JS нет никакого концепта времени, все асинхронные операции происходят вне движка. Call stack слепо исполняет те инструкции, которые ему передаёт event loop, координирующий работу всей программы.
Ад колбеков (callback hell)
Асинхронности в JS-коде можно добиться, используя колбеки (обратные вызовы). Это такие функции-соглашения, которые возвращают результат не сразу, а спустя какое-то время. В колбеки вкладывается код, который должен выполняться после завершения определённой операции – например, загрузки какого-нибудь изображения. То есть пользователь сразу увидит, например, текстовый контент на странице, а не будет ждать его появления только после прогрузки изображения, которое расположено выше. Для обеспечения возможности общения клиента с сервером используются AJAX (от англ. Asynchronous JavaScript and XML) запросы. XML в этой связке сейчас уже нет – в ходу JSON-файлы, но название AJAX закрепилось и осталось неизменным.
Ад колбэков можно узнать по вложенной структуре в коде. Получается что-то вроде «пирамидки». Такой код сложно читать и поддерживать. И чем пирамидальная структура глубже, тем сложнее. Чтобы сделать структуру запросов плоской и упростить ее понимание, была внедрена конструкция промисов.

Промисы: понятие и синтаксис
Промисы помогают работать без ада колбэков. Сам по себе промис – это особый объект, используемый в качестве плейсхолдера для будущего значения завершенной асинхронной операции. Промис (англ. promise – обещать) как бы «обещает» создать это значение, которое на настоящий момент не установлено. Внутри промиса находится исполнительская функция, которая берёт в себя 2 колбэка. Если функция выполняется успешно, то отрабатывает колбэк resolve, который принимает в себя получившийся в результате аргумент. На примере это строка «Все прошло отлично». Если же промис отклоняется и возникает ошибка, получаем reject. На примере это строчка «Что-то пошло не так». Аргументами могуты быть строка, число функция, объект.
< const promise = new Promise((resolve, reject) => < if(allWentWell) < resolve('Все прошло отлично!'); >else < reject('Что-то пошло не так'); >>);
Вот создали мы промис, и что дальше? Нужно настроить сценарии его работы. Чтобы получить его результат и взаимодействовать с ним, используем метод then. На случай ошибки можем использовать для отработки функции метод catch. А еще применим блок finally. Если представить, что промис – это вечеринка, то пройти она может хорошо либо плохо. Однако независимо от того, как она пройдёт, после неё нужно прибраться. Finally – это такой «уборщик» – он знает, что вечеринка закончилась – успешно или нет – он приберётся. Внутри у него нет аргумента, и вне зависимости от успешности операции он нам сообщит, что промис завершён.
< new Promise((resolve, reject) => < setTimeout(() =>resolve("value"), 2000); >) .finally(() => alert("Промис завершен")) // finally отработает первым .then(result => alert(result)); // выводится "value"
< new Promise((resolve, reject) =>< throw new Error("error"); >) .finally(() => alert("Промис завершен")) // finally отработает первым .catch(err => alert(err)); // выводится ошибка
Как работать с асинхронным кодом в javascript

Вот эти возможности браузера: Dev Tools, Console, Sockets, Network Requests, Rendering (HTML DOM), Timer и т. д. JavaScript включает в себя набор функций, которые выглядят обычными, но на самом деле являются средством взаимодействия с веб-браузером. Примерами таких функций являются:
- объект документа, который ссылается на HTML DOM;
- функция fetch, взаимодействующая с Network Requests;
- функция setTimeOut(), ссылающаяся на таймер.
Попробуем разобраться в следующем фрагменте кода:

Большая часть нашего кода JavaScript выполняется с помощью функций браузера, но у нас нет доступа к скрытому в его глубине бекенду. Новая возможность ES6 обещает помочь получить некоторую согласованность между происходящим в фоновом режиме и нашей фактической памятью JavaScript, чтобы обрабатывать результат. В ES5 использовалась fetch/xhr для получения сетевого запроса, но это не влияло на фактическую память. С введением промисов fetch должна совершать сетевой запрос и возвращать объект Promise, который будет находиться в памяти. Как только запрос выполнится, объект будет заполнен данными из него.
Промисы можно лучше понять, опираясь на два подхода. Первый указывает на выполняемые через функции браузера фоновые процессы, а второй является отслеживающим запросом объекта Promise в памяти.
Использование этих двух подходов поможет сделать следующее:

- инициировать фоновую работу веб-браузера;
- немедленно возвратить объект-заполнитель (promise) в JavaScript.
Функция f etch – одна из важных функций J S . С ее помощью устанавливается объект Promise в памяти JS. Он имеет два свойства: value , в котором хранятся данные ответа, и onFulfilled , который является пустым массивом.
Он также инициирует функцию браузера Network Request для отправки запроса HTTP и получения ответа. Все это вместе с запросом хранится в переменной futureData , которая в свою очередь является объектом promise: futureData.value = response data
Мы также должны понять, какова цель onFulfilled[ ] . Это скрытое свойство объекта Promise , к которому нельзя получить доступ. Как только свойство value получает некоторое значение, свойство onFulfilled автоматически выполняет функцию, которая должна использовать данные из ответа – именно она хранится в массиве onFulfilled . В приведенном выше фрагменте кода, как только свойство value получает ответ, запускается функция display , и данные передаются в качестве аргумента для onFulfilled .
На данном этапе может возникнуть вопрос, как добавить функцию к onFulfilled массиву? Мы не можем использовать array.push() , так как это скрытое свойство. Вместо этого применим метод then , что значительно облегчает жизнь.

Еще один важный нюанс – обработка ошибок в Promise , вроде неудачного запроса или нулевых данных. Существует множество случаев, когда сетевой запрос может завершиться неудачей. Чтобы решить эту проблему, объект Promise имеет еще одно скрытое свойство onRejection , которое является пустым массивом.
Мы можем использовать метод .catch() , чтобы добавить к нему функцию. Эта функция будет вызвана onRejection и выполнена в случае, если мы получим ошибку.
И напоследок
Наконец, нам нужно знать, как Promise-deferred функция возвращается в JavaScript для выполнения. В JS есть функция, очень похожая на Callback queue, которая называется Microtask Queue. Пока выполняется сетевой запрос и извлекаются данные, функция, связанная с объектом Promise , помещается в Microtask Queue.
Как только все данные из ответа получены, она извлекается из Microtask Queue, попадает в callstack через event loop и выполняется.
Microtask Queue имеет более высокий приоритет, чем очередь Callback, поэтому функции, связанные с другими асинхронными функциями (вроде setTimeOut() ), выполняются после выполнения функций объекта Promise .
Заключение
Мы рассмотрели важную и ответственную тему и надеемся, что статья окажется для вас полезной. В « Библиотеке программиста » есть несколько материалов, которые помогут ее дополнить:
- async/await в JavaScript: преимущества и подводные камни
- 9 полезных советов по Promise.resolve и Promise.reject
- TOП-12 JavaScript-концепций: от ссылок до асинхронных операций
- Асинхронное программирование: концепция, реализация, примеры
- Цикл событий: как выполняется асинхронный JavaScript-код в Node.js
- Глубокое погружение в асинхронные JavaScript функции
Не останавливайтесь на достигнутом и удачи в обучении!
Источники
JavaScript: методы асинхронного программирования
Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на 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 . Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.
Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:
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)
При таком подходе один взгляд на имена коллбэков в выражениях .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>
Вот что здесь происходит:
- Мы ожидаем подготовки первого запроса, возвращая ссылку на функцию и ожидая коллбэка для этого первого запроса (вспомните функцию 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)
Здесь мы можем индивидуально обрабатывать список репозиториев каждого пользователя. Для того, чтобы улучшить этот код, можно было бы выделить функции обратного вызова, как мы уже делали выше.
Я неоднозначно отношусь к генераторам. С одной стороны, можно быстро понять, чего ожидать от кода, взглянув на генератор, с другой, выполнение генераторов приводит к проблемам, похожим не те, что возникают в аду коллбэков.
Надо отметить, что генераторы — возможность сравнительно новая, как результат, если вы рассчитываете на использование вашего кода в старых версиях браузеров, код надо обработать транспилятором. Кроме того, генераторы в написании асинхронного кода используют нечасто, поэтому, если вы занимаетесь командной разработкой, учтите, что некоторые программисты могут быть с ними незнакомы.
На тот случай, если вы решили лучше вникнуть в эту тему, вот и вот — отличные материалы о внутреннем устройстве генераторов.
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 , в данном случае — это всего одна операция.
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 вы пользуетесь?
- Блог компании RUVDS.com
- Веб-разработка
- JavaScript
