☕ Асинхронный JavaScript для начинающих
Основа современной веб-разработки – асинхронное выполнение кода. В небольшой статье разберемся, как освоить этот подход, используя JavaScript.
Перевод публикуется с сокращениями, автор оригинальной статьи Devanshi Tank.
Однопоточные языки, такие как JavaScript, могут одновременно выполнять только одну задачу, что приводит к ненужным задержкам. Медленные функции блокируют дальнейшее выполнение кода. Чтобы избежать всего этого, необходим асинхронный JavaScript.
Для понимания асинхронности недостаточно знать только core JavaScript. Движок JS состоит из трех основных частей:
- поток выполнения;
- память/переменная среда;
- стек вызовов.
Можно добавить в этот список API браузеров, Promises, Eventloop, Task queue и Microtask queue.
Как мы знаем, JavaScript работает в браузере, а браузер – это мощная программа, предоставляющая множество функций, некоторые из которых делают возможным асинхронный код.
Вот эти возможности браузера: Dev Tools, Console, Sockets, Network Requests, Rendering (HTML DOM), Timer и т. д. JavaScript включает в себя набор функций, которые выглядят обычными, но на самом деле являются средством взаимодействия с веб-браузером. Примерами таких функций являются:
- объект документа, который ссылается на HTML DOM;
- функция fetch, взаимодействующая с Network Requests;
- функция setTimeOut(), ссылающаяся на таймер.
Попробуем разобраться в следующем фрагменте кода:
Определение функции
printHello
будет сохранено в глобальной памяти. Функция setTimeOut
установит
таймер на 1000 мс, чтобы вызвать после его завершения printHello
. Как только setTimeOut
все это проделает, произойдет переход на следующую строку и консоль выведет «Me First».
Тем временем таймер
работает: как только он завершит свою работу, вызовется функция printHello
.
Поток управления вернется JavaScript, и напечатается «Hello».
Это был простой пример, но что делать, когда нужно выполнить тысячи строк асинхронного кода? Необходимы некоторые правила, чтобы поток выполнения работал как нужно.
В приведенном примере, поскольку таймер равен 0 мс, можно было бы предположить, что он немедленно запускает printHello, но этого не происходит.
Для этого JavaScript есть Callback Queue. printHello
помещается в нее и остается
там до тех пор, пока движок JS не выполнит весь синхронный код. Event Loop проверяет стек вызовов на пустоту, а также завершено ли выполнение
всего глобального кода.
Как только все будет
сделано, Event Loop извлекает функцию printHello
из очереди обратного вызова и
помещает ее в стек вызовов.
Функция printHello
выполняется после глобального кода. С этим подходом есть некоторые проблемы.
Предположим, что функция в setTimeOut
извлекает некоторые данные из API, а
затем запускает другую функцию с этими данными, которые мы не сможем контролировать.
Это происходит, поскольку данная функция автоматизирована с помощью
setTimeOut
. Информация, которую она несет и возвращаемый ответ ограничены
ею же. С этой ситуацией легче справиться с помощью Promises.
Promises
Большая часть нашего кода JavaScript выполняется с помощью функций браузера, но у нас нет доступа к скрытому в его глубине бекенду. Новая возможность ES6 обещает помочь получить некоторую
согласованность между происходящим в фоновом режиме и нашей фактической памятью JavaScript, чтобы обрабатывать результат. В ES5 использовалась
fetch/xhr
для получения сетевого запроса, но это не влияло на фактическую
память. С введением промисов fetch
должна совершать сетевой запрос и возвращать
объект Promise, который будет находиться в памяти. Как только запрос выполнится,
объект будет заполнен данными из него.
Промисы можно лучше понять, опираясь на два подхода. Первый указывает на выполняемые через функции браузера фоновые процессы, а второй является отслеживающим запросом объекта Promise в памяти.
Использование этих двух подходов поможет сделать следующее:
- инициировать фоновую работу веб-браузера;
- немедленно возвратить объект-заполнитель (promise) в JavaScript.
Приведенный фрагмент
иллюстрирует простой promise. Получаемое нами обратно из запроса значение хранится в futureData
и в свою очередь передается в качестве параметра (data
) для функции display
. Чтобы понять, как все это работает, рассмотрим,
что именно делает fetch
.
Функция fetch
– одна из важных функций JS. С ее помощью устанавливается объект 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 функции
Не останавливайтесь на достигнутом и удачи в обучении!