Как работает Async/Await в JavaScript
На написание этого поста натолкнула статья How JavaScript Async/Await Works от Thon Ly
Введение
Используя ту или иную технологию, хочется глубже понять как она работает. Руки дошли и до Async/Await. Одним из лучших вариантов, разобраться как это работает, написать собственный полифил. Я продемонстрирую его ближе к концу поста, а пока давайте приведем пример.
Необходимо сделать запрос за некими данными, и после ответа вывести данные в консоль.
Для запроса будем использовать Fetch API — интерфейс для получения ресурсов (в том числе по сети). Запрос с помощью fetch возвращает Promise (обещание — которое будет выполнено или отклонено). Для того чтобы получить данные из промиса, нам нужно включить в цепочку два колбека с помощью метода then.
- Делаем запрос, получаем промис.
- После того как промис выполнен успешно, получаем специальный объект ответа. Выполним метод объекта ответа json(), чтобы получить данные.
- Метод json() возвращает промис, так что далее снова вызываем then и в колбеке выводим данные в консоль.
Все просто 😅
Используя Async/Await можем сделать так:
Мы обернули наш вызов в асинхронную функцию (async), заменили колбеки (then) на заявление await. Теперь код останавливает выполнение на ключевых словах await, до выполнения обещания, и продолжает выполнение далее. Асинхронное программирование как синхронное!
Круто, верно? 😉
На самом же деле Async/Await это просто синтаксический сахар. Я попытаюсь показать, как это работает внутри и для этого мы напишем асинхронную функцию с нуля.
Асинхронная функция
Ключевым моментом в асинхронной функции является использование генераторов (Generators). Давайте напишем функцию-генератор.
Функция генератор сама по себе не выполняется, она лишь создает специальный итерируемый объект знающий о своем состоянии. Этот объект имеет метод next() который позволяет запускать выполнение функции до ключевого слова yield. Инструкция yield, на мой взгляд, похожа на return, возвращает промежуточный результат и приостанавливает выполнение кода.
Но в таком виде, это просто функция генератор. Нам необходимо как то перебрать ее. Для этого напишем свою асинхронную функцию.
И тут распишу подробно.
- Назовем функцию async чтобы было похоже на нативную
- Создаем сам объект генератор
- Создадим функцию next, как и нативный метод. С ее помощью будем перебирать наш генератор. Функция принимает value — тут будем передавать промис полученный на этапе перебора генератора. В первом вызове (строчка 10) ничего не передается.
- В nextResult получаем результат первой итерации генератора. И как вы можете знать, нативный метод next генератора возвращает объект со значением value и done. В value находится промис запроса поста, а в done — булевое значение false, так как генератор еще не закончил свою работу и в его теле еще есть yield.
- Далее проверяем закончено ли выполнение генератора, и если да, просто возвращаем значение. В нашем случае это не так.
- Так как мы получили промис, выполним метод then, и передадим колбеком нами созданную функцию next.
- Далее выполняется следующий кусок кода генератора. И заметьте — теперь функция next получает value — промис из предыдущего вызова. Мы как бы заменяем yield fetch(‘https://jsonplaceholder.typicode.com/posts/1'); на наш промис и продолжаем выполнение.
- Натыкаемся на следующий yield. Метод json() обекта ответа так же возвращает промис. Так что вся процедура повторится еще один раз.
- И в последний раз функция next получает уже готовые данные. Перебор заканчивается, выходим из функции.
Фуххх 😮 Но если вглядеться — все довольно ожидаемо.
Заключение
Async/Await действительно позволяет писать асинхронный код синхронно, не блокируя стек вызовов. Мы замораживаем код и ждем пока выполнится промис, а затем продолжаем. Но внутри все работает именно на генераторах. Особенность асинхронных функций в том что они позволяют “вытащить” значение завершенного промиса без метода then.
Я надеюсь, вам понравился пост. Если да, похлопайте 👏, чтобы помочь другим найти эту информацию. И не стесняйтесь оставлять комментарий — буду рад!