☕ Асинхронный 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(), ссылающаяся на таймер.

Попробуем разобраться в следующем фрагменте кода:

function printHello() 
{ 
  console.log('Hello');
}
setTimeOut(printHello, 1000);
console.log('Me First');

Определение функции printHello будет сохранено в глобальной памяти. Функция setTimeOut установит таймер на 1000 мс, чтобы вызвать после его завершения printHello. Как только setTimeOut все это проделает, произойдет переход на следующую строку и консоль выведет «Me First».

Тем временем таймер работает: как только он завершит свою работу, вызовется функция printHello. Поток управления вернется JavaScript, и напечатается «Hello».

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

function printHello()
{
 console.log('Hello');
}
function blockfor1sec()
{ //блокирует поток на одну секунду }
setTimeOut(printHello, 0);
blockfor1sec();
console.log('Me First');

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

Для этого JavaScript есть Callback Queue. printHello помещается в нее и остается там до тех пор, пока движок JS не выполнит весь синхронный код. Event Loop проверяет стек вызовов на пустоту, а также завершено ли выполнение всего глобального кода.

Как только все будет сделано, Event Loop извлекает функцию printHello из очереди обратного вызова и помещает ее в стек вызовов.

console
---------
Me first
Hello

Функция printHello выполняется после глобального кода. С этим подходом есть некоторые проблемы. Предположим, что функция в setTimeOut извлекает некоторые данные из API, а затем запускает другую функцию с этими данными, которые мы не сможем контролировать. Это происходит, поскольку данная функция автоматизирована с помощью setTimeOut. Информация, которую она несет и возвращаемый ответ ограничены ею же. С этой ситуацией легче справиться с помощью Promises.

Promises

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

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

Использование этих двух подходов поможет сделать следующее:

  • инициировать фоновую работу веб-браузера;
  • немедленно возвратить объект-заполнитель (promise) в JavaScript.
function display(data)
{
  console.log(data);
}
const futureData = fetch('https//somelink.com');
futureData.then(display);
console.log('Me first');

Приведенный фрагмент иллюстрирует простой 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.

Заключение

Мы рассмотрели важную и ответственную тему и надеемся, что статья окажется для вас полезной. В «Библиотеке программиста» есть несколько материалов, которые помогут ее дополнить:

Не останавливайтесь на достигнутом и удачи в обучении!

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...