что такое асинхронность в javascript
Асинхронность в JavaScript: Пособие для тех, кто хочет разобраться
На JavaScript легко писать. Достаточно взять пару библиотек или модный фреймворк, прочитать несложный туториал и все — через пару часов у вас простой работающий интерфейс.
Проблемы начинаются, когда интерфейс становится сложнее. Вот тут без глубокого понимания JavaScript не обойтись. Важно, чтобы даже большой и сложный интерфейс оставался быстрым и отзывчивым. Отзывчивость, как правило, достигается за счет использования асинхронных функций. Попробуем разобраться, как устроена асинхронность в JavaScript.
В JavaScript нет многопоточности. Несмотря на то, что мы уже можем полноценно использовать вебворкеры, из них нельзя менять DOM или вызывать методы объекта window. Одним словом, не многопоточность, а сплошное разочарование.
Причины таких ограничений понятны. Представьте себе, что два параллельных потока пытаются наперегонки поменять один и тот же узел в DOM с непредсказуемым результатом. Представили? Мне тоже стало не по себе.
С DOM-деревом работают в одном потоке, чтобы гарантировать целостность и непротиворечивость данных, но как программировать интерфейс с одним потоком? Ведь сама суть интерфейса — в асинхронности. Именно для этого придуманы асинхронные функции. Они выполняются не сразу, а после наступления события. Интересно, что эти функции — не часть JavaScript-движков. Вызов setTimeout на чистом V8 приводит к ошибке, так как в V8 нет такой функции. Тогда откуда же появляется setTimeout или requestAnimationFrame или addEventListener?
Асинхронность внутри
Движок JavaScript похож на мясорубку, бесконечно перемалывающую операции, которые последовательно берутся из стека вызовов (1). Код выполняется линейно и последовательно. Удалить операцию из стека нельзя, можно только прервать поток выполнения. Поток выполнения прерывается, если вызвать что-то типа alert или «исключение».
Каждая операция содержит контекст — некую область памяти, из которой доступны данные. Контексты расположены в памяти в виде дерева. Каждому листу в дереве доступны области видимости, которые определены в родительских ветках и в корне (глобальной области видимости). Функции в JavaScript — это данные, они хранятся в памяти именно как данные и поэтому передаются как переменные или возвращаются из других функций.
Асинхронные операции выполняются не в движке, а в окружении (5,6). (Как подсказал forgotten это не совсем так: мы можем из стека вызовов сразу же положить функцию в очередь вызовов и таким образом чистый движок тоже будет работать асинхронно)
Окружение — надстройка на движком. NodeJS и Chrome для движка V8 и Firefox для Gecko. Иногда окружение еще называют web API.
Чтобы создать асинхронный вызов, в web API передается ссылка на функцию, которая выполнится позже или не выполнится вовсе.
У функции есть свой контекст или своя область памяти (3), в которой она определена. Функция имеет доступ к этой области памяти и ко всем родителям этой области памяти. Такие функции называются замыканиями. С этой точки зрения, все функции в JavaScript — замыкания, так как все они имеют контекст.
Web API и JavaScrtipt движок работают независимо. Web API решает, в какой момент функция двигается дальше, в очередь вызовов (2).
Функции в очереди вызовов попадают в JavaScript-движок, где выполняются по одной. Выполнение происходит в том же порядке, в котором функции попадают в очередь.
Окружение самостоятельно решает, когда добавить переданный ей код в очередь вызовов. Функции из очереди добавляются в стек выполнения (выполняются) не раньше, чем стек вызовов закончит работу над текущей функцией.
Таким образом, стек вызовов работает синхронно, а web API асинхронно.
Это очень важно! Разработчику не нужно самому контролировать параллельный доступ к ресурсам, асинхронную работу за него выполняет окружение. Окружения определяют различия между браузером и node.js, ведь на node.js мы пишем сетевые приложения или обращаемся напрямую к жесткому диску, а из Chrome перехватываем клики по кнопкам, используя один и тот же движок.
В очереди вызовов нельзя отменять отдельные операции. Это делается в окружении (removeEventListener — в качестве примера).
Примеры
Можно загрузить стек вызовов так, чтобы он работал бесконечно и следующая функция из очереди вызовов не вызвалась. Попробуйте, например, запустить вот такой код.
Обработчик клика не сработает, а бесконечный цикл загрузит процессор компьютера. Вкладка зависнет 😉
Клик вызовет «тяжелую» для расчета функцию. После клика в консоль пишется start, в конце выполнения функции — end. Выполнение функции на моем ноутбуке занимает несколько секунд. Все время, пока выполняется функция, квадратик мигает. Это значит, что анимации в CSS выполняются асинхронно JavaScript-коду.
Но что будет, если вместо opacity менять размер?
Квадратик зависнет на время выполнения функции. Дело в том, что CSS-свойство height обращается к DOM. Как мы помним, к DOM можно обращаться только из одного потока, чтобы не было проблем с параллельным доступом.
Делаем вывод, что для анимации лучше пользоваться свойствами, которые не меняют DOM (transform, opacity и т.д.). А всю тяжелую работу в JavaScript лучше делать асинхронно. Например вот так.
Код написан для наглядности и на коленке, в бою применять не рекомендуется. Мы делим большой кусок работы на маленькие и выполняем асинхронно. При этом интерфейс не блокируется. Для таких расчетов можно пользоваться веб-воркерами.
Вывод
Благодаря JavaScript мы пишем асинхронные приложения, не задумываясь о многопоточности: о целостности и непротиворечивости данных. За эти преимущества мы платим огромным числом обратных вызовов, блокированием основного потока и постоянными потерями контекста.
О том, как бороться с последней проблемой, я расскажу в следующий раз.
Разбираемся с асинхронностью в JavaScript [Перевод статьи Sukhjinder Arora]
Привет, Хабр! Представляю вашему вниманию перевод статьи «Understanding Asynchronous JavaScript» автора Sukhjinder Arora.
От автора перевода: Надеюсь перевод данной статьи поможет вам ознакомиться с чем-то новым и полезным. Если статья вам помогла, то не поленитесь и поблагодарите автора оригинала. Я не претендую на звание профессионального переводчика, я только начинаю переводить статьи и буду рад любым содержательным фидбекам.
JavaScript — это однопоточный язык программирования, в котором может быть выполнено только что-то одно за раз. То есть, в одном потоке движок JavaScript может обработать только 1 оператор за раз.
Хоть однопоточные языки и упрощают написание кода, поскольку вы можете не беспокоиться о вопросах параллелизма, это также означает, что вы не сможете выполнять долгие операции, такие как доступ к сети, не блокируя основной поток.
Представьте запрос к API для получения некоторых данных. В зависимости от ситуации, серверу может потребоваться некоторое время для обработки вашего запроса, при этом будет заблокировано выполнение основного потока из-за чего ваша веб-страница перестанет отвечать на запросы к ней.
Здесь то и вступает в игру асинхронность JavaScript. Используя асинхронность JavaScript(функции обратного вызова(callback’и), “промисы” и async/await) вы можете выполнять долгие сетевые запросы без блокирования основного потока.
Несмотря на то, что не обязательно изучать все эти концепции, чтобы быть хорошим JavaScript-разработчиком, полезно их знать.
Итак, без лишних слов, давайте начинать.
Как работает синхронный JavaScript?
Прежде чем мы углубимся в работу асинхронного JavaScript, давайте для начала разберемся как выполняется синхронный код внутри движка JavaScript. Например:
Для того, чтобы разобраться как код выше выполняется внутри движка JavaScript, нам следует понимать концепцию контекста выполнения и стека вызовов(так же известный как стек выполнения).
Контекст выполнения
Контекст выполнения — это абстрактное понятие окружения, в котором код оценивается и выполняется. Всякий раз, когда какой-либо код выполняется в JavaScript он запускается в контексте выполнения.
Код функции выполняется внутри контекста выполнения функции, а глобальный код в свою очередь выполняется внутри глобального контекста выполнения. Каждая функция имеет свой собственный контекст выполнения.
Стек вызовов
Под стеком вызовов подразумевается стек со структурой LIFO(Last in, First Out/Последний вошел, первый вышел), который используется для хранения всех контекстов выполнения, созданных на протяжении исполнения кода.
В JavaScript имеется только один стек вызовов, так как это однопоточный язык программирования. Структура LIFO означает, что элементы могут добавляться и удаляться только с вершины стека.
Давайте теперь вернемся к фрагменту кода выше и попробуем понять, как движок JavaScript его выполняет.
И так, что же здесь произошло?
Когда код начал выполняться, был создан глобальный контекст выполнения(представленный как main()) и добавлен на вершину стека вызовов. Когда встречается вызов функции first(), он так же добавляется на вершину стека.
Далее, на вершину стека вызовов помещается console.log(‘Hi there!’), после выполнения он удаляется из стека. После этого мы вызываем функцию second(), поэтому она помещается на вершину стека.
console.log(‘Hello there!’) добавлен на вершину стека и удаляется из него по завершению выполнения. Функция second() завершена, она также удаляется из стека.
console.log(‘The End’) добавлен на вершину стека и удален по завершению. После этого функция first() завершается и также удаляется из стека.
Выполнение программы заканчивается, поэтому глобальный контекст вызова(main()) удаляется из стека.
Как работает асинхронный JavaScript?
Теперь, когда мы имеем общее представление о стеке вызовов и о том, как работает синхронный JavaScript, давайте вернемся к асинхронному JavaScript.
Что такое блокирование?
Давайте предположим, что мы выполняем обработку изображения или сетевой запрос синхронно. Например:
Обработка изображения и сетевой запрос требует времени. Когда функция processImage() вызвана её выполнение потребует некоторого времени, в зависимости от размера изображения.
Когда функция processImage() выполнена она удаляется из стека. После нее вызывается и добавляется в стек функция networkRequest(). Это снова займет некоторое время прежде чем завершить выполнение.
В конце концов, когда функция networkRequest() выполнена, вызывается функция greeting(), поскольку она содержит только метод console.log, а этот метод, как правило, выполняется быстро, функция greeting() выполнится и завершится мгновенно.
Как вы видите, нам нужно ждать пока функция(такие как processImage() или networkRequest()) завершится. Это означает, что такие функции блокируют стек вызовов или основной поток. По итогу мы не можем выполнить другие операции, пока код выше не будет выполнен.
Так какое же решение?
Самое простое решение — это асинхронные функции обратного вызова. Мы используем их, чтобы сделать наш код неблокируемым. Например:
Здесь я использовал метод setTimeout для того чтобы имитировать сетевой запрос. Пожалуйста, помните, что setTimeout не является частью движка JavaScript, это часть так называемого web API(в браузере) и C/C++ APIs (в node.js).
Для того чтобы понять, как этот код выполняется, мы должны разобраться с ещё несколькими понятиями, такими как цикл обработки событий и очередь обратных вызовов(также известная как очередь задач или очередь сообщений).
Цикл обработки событий, web API и очередь сообщений/очередь задач не являются частью движка JavaScript, это часть браузерной среды выполнения JavaScript или среды выполнения JavaScript в Nodejs(в случае Nodejs). В Nodejs, web APIs заменяется на C/C++ APIs.
Теперь давайте вернемся назад, к коду выше, и посмотрим, что произойдет в случае асинхронного выполнения.
Когда код приведенный выше загружается в браузер console.log(‘Hello World’) добавляется в стек и удаляется из него по завершению выполнения. Далее встречается вызов функции networkRequest(), он добавляется на вершину стека.
Следующая вызывается функция setTimeout() и помещается на вершину стека. Функция setTimeout() имеет 2 аргумента: 1) функция обратного вызова и 2) время в миллисекундах.
setTimeout() запускает таймер на 2 секунды в окружении web API. На этом этапе, setTimeout() завершается и удаляется из стека. После этого, в стек добавляется console.log(‘The End’), выполняется и удаляется из него по завершению.
Тем временем таймер истек, теперь обратный вызов добавляется в очередь сообщений. Но обратный вызов не может быть немедленно выполнен, и именно здесь в процесс вступает цикл обработки событий.
Цикл обработки событий
Задача цикла обработки событий заключается в том чтобы следить за стеком вызовов и определять пуст он или нет. Если стек вызовов пустой, то цикл обработки событий заглядывает в очередь сообщений, чтобы узнать есть ли обратные вызовы, которые ожидают своего выполнения.
В нашем случае очередь сообщений содержит один обратный вызов, а стек выполнения пуст. Поэтому цикл обработки событий добавляет обратный вызов на вершину стека.
После console.log(‘Async Code’) добавляется на вершину стека, выполняется и удаляется из него. На этом моменте обратный вызов выполнен и удален из стека, а программа полностью завершена.
События DOM
Очередь сообщений также содержит обратные вызовы от событий DOM, такие как клики и “клавиатурные” события. Например:
В случае с событиями DOM, обработчик событий находится в окружении web API, ожидая определенного события(в данном случае клик), и когда это событие происходит функция обратного вызова помещается в очередь сообщений, ожидая своего выполнения.
Мы изучил как выполняются асинхронные обратные вызовы и события DOM, которые используют очередь сообщений для хранения обратных вызовов ожидающих своего выполнения.
ES6 Очередь микротасков
Прим. автора перевода: В статье автор использовал message/task queue и job/micro-taks queue, но если перевести task queue и job queue, то по идее это получается одно и то же. Я поговорил с автором перевода и решил просто опустить понятие job queue. Если у вас есть какие-то свои мысли на этот счет, то жду вас в комментариях
ES6 представил понятие очередь микротасков, которые используются “промисами” в JavaScript. Разница между очередью сообщений и очередью микротасков состоит в том, что очередь микротасков имеет более высокий приоритет по сравнению с очередью сообщений, это означает, что “промисы” внутри очереди микротасков будут выполняться раньше, чем обратные вызовы в очереди сообщений.
Как вы можете видеть, “промис” выполнился раньше setTimeout, все это из-за того, что ответ “промиса” хранится внутри очереди микростасков, которая имеет более высокий приоритет, нежели очередь сообщений.
Давайте разберем следующий пример, на этот раз 2 “промиса” и 2 setTimeout:
И снова оба наших “промиса” выполнились раньше обратных вызовов внутри setTimeout, так как цикл обработки событий считает задачи из очереди микротасков важнее задач из очереди сообщений/очереди задач.
Если во время выполнения задач из очереди микротасков появляется ещё один “промис”, то он будет добавлен в конец этой очереди и выполнен раньше обратных вызовов из очереди сообщений, и не важно сколько времени они ожидают своего выполнения.
Таким образом, все задачи из очереди микротасков будут выполнены раньше задач из очереди сообщений. То есть цикл обработки событий сначала очистит очередь микротасков, а только после этого начнет выполнение обратных вызовов из очереди сообщений.
JavaScript: методы асинхронного программирования
Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.
Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.
Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.
О синхронном и асинхронном коде
Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:
Он, без особых сложностей, выводит в консоль числа от 1 до 3.
Теперь — код асинхронный:
Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.
Постановка задачи
Предположим, перед нами стоит задача поиска пользователя GitHub и загрузки данных о его репозиториях. Главная проблема тут в том, что мы не знаем точного имени пользователя, поэтому нам нужно вывести всех пользователей с именами, похожими на то, что мы ищем, и их репозитории.
В плане интерфейса ограничимся чем-нибудь простым.
Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.
Функции обратного вызова
С функциями в JS можно делать очень много всего, в том числе — передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:
Используя этот подход для решения нашей задачи, мы можем написать такую функцию request :
Разберём то, что здесь происходит:
Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:
Ад коллбэков во всей красе. Изображение взято отсюда.
В данном случае под «состоянием гонки» мы понимаем ситуацию, когда мы не контролируем порядок получения данных о репозиториях пользователей. Мы запрашиваем данные по всем пользователям, и вполне может оказаться так, что ответы на эти запросы окажутся перемешанными. Скажем, ответ по десятому пользователю придёт первым, а по второму — последним. Ниже мы поговорим о возможном решении этой проблемы.
Промисы
Используя промисы можно улучшить читабельность кода. В результате, например, если в ваш проект придёт новый разработчик, он быстро поймёт, как там всё устроено.
Для того, чтобы создать промис, можно воспользоваться такой конструкцией:
Разберём этот пример:
Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.
Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён
Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:
На самом деле, это лишь вершина айсберга того, что называется промисами. Вот материал, который я рекомендую почитать тем, кто хочет более основательно погрузиться в эту тему.
Генераторы
Ещё один подход к решению нашей задачи, который, однако, нечасто встретишь — это генераторы. Тема это немного более сложная, чем остальные, поэтому, если вы чувствуете, что вам это изучать пока рано, можете сразу переходить к следующему разделу этого материала.
Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request :
Генератор будет выглядеть так:
Вот что здесь происходит:
Здесь мы можем индивидуально обрабатывать список репозиториев каждого пользователя. Для того, чтобы улучшить этот код, можно было бы выделить функции обратного вызова, как мы уже делали выше.
Я неоднозначно отношусь к генераторам. С одной стороны, можно быстро понять, чего ожидать от кода, взглянув на генератор, с другой, выполнение генераторов приводит к проблемам, похожим не те, что возникают в аду коллбэков.
Надо отметить, что генераторы — возможность сравнительно новая, как результат, если вы рассчитываете на использование вашего кода в старых версиях браузеров, код надо обработать транспилятором. Кроме того, генераторы в написании асинхронного кода используют нечасто, поэтому, если вы занимаетесь командной разработкой, учтите, что некоторые программисты могут быть с ними незнакомы.
На тот случай, если вы решили лучше вникнуть в эту тему, вот и вот — отличные материалы о внутреннем устройстве генераторов.
Async/await
Здесь происходит следующее:
Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.
Итоги
В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.
Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?
Руководство по Node.js, часть 7: асинхронное программирование
Сегодня, в переводе седьмой части руководства по Node.js, мы поговорим об асинхронном программировании, рассмотрим такие вопросы, как использование коллбэков, промисов и конструкции async/await, обсудим работу с событиями.
Асинхронность в языках программирования
Сам по себе JavaScript — это синхронный однопоточный язык программирования. Это означает, что в коде нельзя создавать новые потоки, выполняющиеся параллельно. Однако компьютеры, по своей природы, асинхронны. То есть некие действия могут выполняться независимо от главного потока выполнения программы. В современных компьютерах каждой программе выделяется некое количество процессорного времени, когда это время истекает, система отдаёт ресурсы другой программе, тоже на некоторое время. Подобные переключения выполняются циклически, делается это настолько быстро, что человек попросту не может этого заметить, в результате мы думаем, что наши компьютеры выполняют множество программ одновременно. Но это иллюзия (если не говорить о многопроцессорных машинах).
В недрах программ используются прерывания — сигналы, передаваемые процессору и позволяющие привлечь внимание системы. Не будем вдаваться в детали, самое главное — помните о том, что асинхронное поведение, когда выполнение программы приостанавливается до того момента, когда ей понадобятся ресурсы процессора, это совершенно нормально. В то время, когда программа не нагружает систему работой, компьютер может решать другие задачи. Например, при таком подходе, когда программа ждёт ответа на выполненный ей сетевой запрос, она не блокирует процессор до момента получения ответа.
Как правило, языки программирования являются асинхронными, некоторые из них дают программисту возможность управлять асинхронными механизмами, пользуясь либо встроенными средствами языка, либо специализированными библиотеками. Речь идёт о таких языках, как C, Java, C#, PHP, Go, Ruby, Swift, Python. Некоторые из них позволяют программировать в асинхронном стиле, используя потоки, запуская новые процессы.
Асинхронность в JavaScript
Как уже было сказано, JavaScript — однопоточный синхронный язык. Строки кода, написанного на JS, выполняются в том порядке, в котором они присутствуют в тексте, друг за другом. Например, вот вполне обычная программа на JS, демонстрирующая такое поведение:
Ответ кроется в окружении, в котором работает JavaScript. А именно, эффективно решать подобные задачи позволяет браузер, давая в распоряжение программиста соответствующие API.
В окружении Node.js имеются средства для выполнения неблокирующих операций ввода-вывода, таких, как работа с файлами, организация обмена данными по сети и так далее.
Коллбэки
Если говорить о браузерном JavaScript, то можно отметить, что нельзя заранее узнать, когда пользователь щёлкнет по некоей кнопке. Для того чтобы обеспечить реакцию системы на подобное событие, для него создают обработчик.
Обработчик события принимает функцию, которая будет вызвана при возникновении события. Выглядит это так:
Такие функции ещё называют функциями обратного вызова или коллбэками.
Коллбэк — это обычная функция, которая передаётся, как значение, другой функции. Вызвана она будет только в том случае, когда произойдёт некое событие. В JavaScript реализована концепция функций первого класса. Такие функции можно назначать переменным и передавать другим функциям (называемым функциями высшего порядка).
Коллбэки используются повсеместно, а не только для обработки событий DOM. Например, мы уже встречались с их использованием в таймерах:
В XHR-запросах тоже используются коллбэки. В данном случае это выглядит как назначение функции соответствующему свойству. Подобная функция будет вызвана при возникновении определённого события. В следующем примере таким событием является изменение состояния запроса:
▍Обработка ошибок в коллбэках
▍Проблема коллбэков
Коллбэками удобно пользоваться в простых ситуациях. Однако, каждый коллбэк — это дополнительный уровень вложенности кода. Если используется несколько вложенных коллбэков, это быстро приводит к значительному усложнению структуры кода:
В этом примере показано всего лишь 4 уровня кода, но на практике можно столкнуться и с большим количеством уровней, обычно это называют «адом коллбэков». Справиться с этой проблемой можно, используя другие языковые конструкции.
Промисы и async/await
Начиная со стандарта ES6 в JavaScript появляются новые возможности, которые облегчают написание асинхронного кода, позволяя обходиться без коллбэков. Речь идёт о промисах, которые появились в ES6, и о конструкции async/await, появившейся в ES8.
▍Промисы
Промисы (promise-объекты) — это один из способов работы с асинхронными программными конструкциями в JavaScript, который, в целом, позволяет сократить использование коллбэков.
Знакомство с промисами
Промисы обычно определяют как прокси-объекты для неких значений, появление которых ожидается в будущем. Промисы ещё называют «обещаниями» или «обещанными результатами». Хотя эта концепция существует уже многие годы, промисы были стандартизированы и добавлены в язык лишь в ES2015. В ES2017 появилась конструкция async/await, которая основана на промисах, и которую можно рассматривать в качестве их удобной замены. Поэтому, даже если не планируется пользоваться обычными промисами, понимание того, как они работают, важно для эффективного использования конструкции async/await.
Как работают промисы
После вызова промиса он переходит в состояние ожидания (pending). Это означает, что функция, вызвавшая промис, продолжает выполняться, при этом в промисе производятся некие вычисления, по завершении которых промис сообщает об этом. Если операция, которую выполняет промис, завершается успешно, то промис переводится в состояние «выполнено» (fulfilled). О таком промисе говорят, что он успешно разрешён. Если операция завершается с ошибкой, промис переводится в состояние «отклонено» (rejected).
Поговорим о работе с промисами.
Создание промисов
Работа с промисами
Выше мы создали промис, теперь рассмотрим работу с ним. Выглядит это так:
Объединение промисов в цепочки
Рассмотрим следующий пример объединения промисов в цепочки:
Здесь мы пользуемся npm-пакетом node-fetch и ресурсом jsonplaceholder.typicode.com в качестве источника JSON-данных.
В данном примере функция fetch() применяется для загрузки элемента TODO-списка с использованием цепочки промисов. После выполнения fetch() возвращается ответ, имеющий множество свойств, среди которых нас интересуют следующие:
В данном случае мы возвращаем обработанные JSON-данные, поэтому третий промис получает именно их, после чего они, предварённые сообщением о том, что в результате запроса удалось получить нужные данные, выводятся в консоль.
Обработка ошибок
Каскадная обработка ошибок
Теперь рассмотрим несколько полезных методов, используемых для управления промисами.
Promise.all()
В ES2015 появился синтаксис деструктурирующего присваивания, с его использованием можно создавать конструкции следующего вида:
Promise.race()
Команда Promise.race() позволяет выполнить заданное действие после того, как будет разрешён один из переданных ей промисов. Соответствующий коллбэк, содержащий результаты этого первого промиса, вызывается лишь один раз. Рассмотрим пример:
Об ошибке Uncaught TypeError, которая встречается при работе с промисами
▍Конструкция async/await
Конструкция async/await представляет собой современный подход к асинхронному программированию, упрощая его. Асинхронные функции можно представить в виде комбинации промисов и генераторов, и, в целом, эта конструкция представляет собой абстракцию над промисами.
Конструкция async/await позволяет уменьшить объём шаблонного кода, который приходится писать при работе с промисами. Когда промисы появились в стандарте ES2015, они были направлены на решение проблемы создания асинхронного кода. Они с этой задачей справились, но за два года, разделяющие выход стандартов ES2015 и ES2017, стало понятно, что считать их окончательным решением проблемы нельзя.
Одной из проблем, которую решали промисы, был знаменитый «ад коллбэков», но они, решая эту проблему, создали собственные проблемы схожего характера.
Промисы представляли собой простые конструкции, вокруг которых можно было бы построить нечто, обладающее более простым синтаксисом. В результате, когда пришло время, появилась конструкция async/await. Её использование позволяет писать код, который выглядит как синхронный, но при этом является асинхронным, в частности, не блокирует главный поток.
Как работает конструкция async/await
Асинхронная функция возвращает промис, как, например, в следующем примере:
Объединим два вышеприведённых фрагмента кода и исследуем его поведение:
Этот код выведет следующее:
Текст I did something попадёт в консоль с задержкой в 3 секунды.
О промисах и асинхронных функциях
Эта конструкция аналогична такой:
Сильные стороны async/await
Анализируя вышеприведённые примеры, можно видеть, что код, в котором применяется async/await, оказывается проще, чем код, в котором используется объединение промисов в цепочки, или код, основанный на функциях обратного вызова. Здесь мы, конечно, рассмотрели очень простые примеры. В полной мере ощутить вышеозначенные преимущества можно, работая с гораздо более сложным кодом. Вот, например, как загрузить и разобрать JSON-данные с использованием промисов:
Вот как выглядит решение той же задачи с использованием async/await:
Использование последовательностей из асинхронных функций
Асинхронные функции легко можно объединять в конструкции, напоминающие цепочки промисов. Результаты такого объединения, однако, отличаются гораздо лучшей читабельностью:
Этот код выведет следующий текст:
Упрощённая отладка
Промисы сложно отлаживать, так как при их использовании нельзя эффективно пользоваться обычными инструментами отладчика (наподобие «шага с обходом», step-over). Код же, написанный с использованием async/await, можно отлаживать с использованием тех же методов, что и обычный синхронный код.
Генерирование событий в Node.js
Для того чтобы вызвать это событие, используется следующая конструкция:
В результате выполнения этой команды вызывается обработчик события и строка started попадает в консоль.
Обработчику событий можно передавать аргументы, представляя их в виде дополнительных аргументов метода emit() :
Похожим образом поступают и в случаях, когда обработчику надо передать несколько аргументов:
Объекты класса EventEmitter имеют и некоторые другие полезные методы: