Используй Async/Await в JavaScript, как профессионал
![]()
В жизни каждого программиста наступает такой момент, когда нужно разобраться с тем, как работает асинхронный код.
Пытающимся впервые понять, как и что здесь происходит, бывает порой страшновато. Но попробуем разобраться. Прежде всего, нужно понять разницу между синхронным и асинхронным кодом.
Что такое «асинхронный код»?
Асинхронность имеет место при наличии как минимум двух событий, происходящих в разное время. Рассмотрим пример:
Здесь код выполняется в том порядке, какой и ожидается. Такой код называется синхронным.
Многое из того, что мы пишем, считается синхронным. Но попробуем отложить выполнение одной из этих строк так, чтобы другие выполнились раньше. Что произойдет тогда?
Что произошло с выводом?
В выводе теперь нарушен порядок слов, а для английского языка порядок слов важен. Поэтому предложение больше не имеет смысла. На самом деле код должен выполняться одновременно, дождаться окончания времени ожидания и продолжить выполнение. Но не в этом случае.
А произошло вот что: функция времени ожидания timeout была вызвана одновременно с выполнением остальной части кода, но запуск внутреннего log произошел спустя 500 мс после этого. Отсюда и выдача неверного вывода.
Строка кода выполнилась в другое время, нежели остальные. Это и называется асинхронностью.
И как же решить эту проблему?
Для этого нужен способ приостановить выполнение остальной части кода до завершения функции времени ожидания timeout . К счастью, в JavaScript есть необходимые инструменты, чтобы именно так и сделать.
Промисы
Промисы похожи на функции обратного вызова: тоже ожидают завершения внутреннего кода, прежде чем возвращать значение.
Разница здесь в том, что после завершения асинхронного кода промис возвращает состояние, позволяющее выполнять дальнейшие действия с помощью таких методов, как then() и catch() .
Примечание: в этой статье не будем слишком подробно разбирать промисы, нам важно иметь общее представление о них.
Рассмотрим следующий пример. Вывод снова в норме:
Но как это получилось?
Итак, мы выяснили, что промисы позволяют дождаться завершения асинхронного кода, прежде чем будет выполнена остальная часть кода. В этом примере мы снова взяли функцию времени ожидания timeout и обернули ее в промис.
После завершения функции времени ожидания timeout промис возвращает resolve и коду сообщается о возможности продолжения выполнения. После успешного выполнения промиса для такого же, как раньше, выполнения остальной части синхронного кода задействуется then() .
А что будет с выводом, если вместо resolve промис вернет reject ?
Когда промис возвращает reject , вызывается метод catch() . В нашем случае этот метод регистрирует сообщение об ошибке и завершает остальную часть кода. Когда это происходит, регистрировать ошибку не нужно.
То есть, условно говоря: когда промис возвращает reject , мы продолжаем работать с остальной частью кода, а не останавливаемся на ошибке.
Введение промисов значительно упрощает работу с асинхронным кодом. Но чем больше в нем цепочка промисов, тем более сложным и менее удобным для восприятия становится код.
Но есть ли более лучшее решение?
Вводим async/await
Используя ключевые слова async/await в JavaScript, мы делаем код гораздо более отточенным и удобным для восприятия. Посмотрим, что произойдет с кодом при задействовании async/await в предыдущем примере:
Теперь код выглядит намного лучше!
Вывод остался таким же, как и раньше. Но такое написание кода позволяет уйти от цепочки промисов, сохраняя при этом удобство для восприятия человеком.
Взяли тот же самый асинхронный код из прошлого примера, только на этот раз предварили функцию ключевым словом await и сохранили получаемый от промиса результат.
При запуске кода функция main ждет, когда async_func вернет resolve , а затем продолжает выполнение остальной части кода.
Мы также обернули остальную часть синхронного кода в функцию main() и задействовали ключевое слово async . Не у одних промисов есть правила — чтобы все это работало, нужно соблюдать правила async/await .
Эти правила гласят: для того, чтобы использовать ключевое слово await , нужно поместить его в функцию async . Этим функция как бы уведомляется о том, что внутри ожидается асинхронный код.
Предположим, теперь нам надо добавить в пример еще одну часть асинхронного кода. Что будет в этом случае?
Обратите внимание: здесь мы добавляем еще один кусок асинхронного кода и вывод остается в том же порядке, какой ожидался.
Посмотрите: для второй асинхронной функции выполнение задано на 30 мс раньше первой. Так почему же выполнение не произошло в таком порядке?
В синхронной ситуации вторая функция действительно была бы вызвана раньше первой и произошла бы выдача неверного вывода.
Но при задействовании async/await код останавливается на первой функции и ждет 530 мс до возвращения resolve . Затем добирается до второй функции и ждет 500 мс также до возвращения resolve . В этом и заключается мощь async/await .
Рассмотрим более практический пример с использованием async/await :
В этом примере мы имитируем вызов к бэкенду и получаем список персонажей, который необходимо проверить на наличие самозванцев.
Задействовав await , мы ждем в функции main возвращения промисом resolve , а в ответ получаем запрошенный список персонажей.
Затем используем метод reduce , чтобы получить счетчик каждого персонажа и сохранить индекс, по которому они нашлись, вместе с именем.
После этого выполняем фильтрацию объекта counts для любых персонажей, счетчик которых равен единице, а затем сопоставляем с массивом imposters .
Дальше отображаем список imposters и показываем, где они находятся в данных о персонаже.
Если бы мы в этом практическом примере обошлись без async/await , то имели бы дело с ошибками в коде.
Не каждый асинхронный код предупреждает о проблеме так очевидно, поэтому неплохо было бы научиться понимать, где возникают эти ситуации, и наилучшим образом с ними справляться.
Заключение
Ну вот и все. Понять, как работать с асинхронным кодом, бывает непросто, особенно когда имеешь с ним дело впервые. Чем больше сталкиваешься с асинхронным кодом, тем лучше с такими ситуациями справляешься.
Надеюсь, вы получили некоторое представление о том, как работает async/await и очень скоро станете еще большим профессионалом в JavaScript.
Async/await
Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.
Асинхронные функции
Начнём с ключевого слова async . Оно ставится перед функцией, вот так:
У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.
Например, эта функция возвратит выполненный промис с результатом 1 :
Можно и явно вернуть промис, результат будет одинаковым:
Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await , которое можно использовать только внутри async -функций.
Await
Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.
В этом примере промис успешно выполнится через 1 секунду:
В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».
Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.
По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then .
Если мы попробуем использовать await внутри функции, объявленной без async , получим синтаксическую ошибку:
Ошибки не будет, если мы укажем ключевое слово async перед объявлением функции. Как было сказано раньше, await можно использовать только внутри async –функций.
Давайте перепишем пример showAvatar() из раздела Цепочка промисов с помощью async/await :
- Нам нужно заменить вызовы .then на await .
- И добавить ключевое слово async перед объявлением функции.
Получилось очень просто и читаемо, правда? Гораздо лучше, чем раньше.
Программисты, узнав об await , часто пытаются использовать эту возможность на верхнем уровне вложенности (вне тела функции). Но из-за того, что await работает только внутри async –функций, так сделать не получится:
Можно обернуть этот код в анонимную async –функцию, тогда всё заработает:
Как и promise.then , await позволяет работать с промис–совместимыми объектами. Идея в том, что если у объекта можно вызвать метод then , этого достаточно, чтобы использовать его с await .
В примере ниже, экземпляры класса Thenable будут работать вместе с await :
Когда await получает объект с .then , не являющийся промисом, JavaScript автоматически запускает этот метод, передавая ему аргументы – встроенные функции resolve и reject . Затем await приостановит дальнейшее выполнение кода, пока любая из этих функций не будет вызвана (в примере это строка (*) ). После чего выполнение кода продолжится с результатом resolve или reject соответственно.
Для объявления асинхронного метода достаточно написать async перед именем:
Разбираемся с асинхронностью в 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, а также ознакомимся с несколькими асинхронными методами, демонстрирующими как они могут помочь нам подобные проблемы решить.
| Необходимое условие: | Базовая компьютерная грамотность, достаточное понимание основ JavaScript. |
|---|---|
| Цель: | Ознакомиться с тем, что такое асинхронный JavaScript, чем он отличается от синхронного и в каких случаях используется. |
Синхронный JavaScript
Чтобы (позволить нам) понять что есть асинхронный JavaScript, нам следовало бы для начала убедиться, что мы понимаем что такое синхронный JavaScript. Этот раздел резюмирует некоторую информацию из прошлой статьи.
Большая часть функциональности, которую мы рассматривали в предыдущих обучающих модулях, является синхронной — вы запускаете какой-то код, а результат возвращается, как только браузер может его вернуть. Давайте рассмотрим простой пример ( посмотрите онлайн, как это работает и посмотрите исходный код):
В этом блоке кода команды выполняются одна за другой:
- Получаем ссылку на элемент <button> , который уже есть в DOM.
- Добавляем к кнопке обработчик события click так, что при нажатии на неё:
- Выводится сообщение alert() .
- После закрытия сообщения создаём элемент <p> (абзац).
- Затем добавляем в абзац текст.
- В конце добавляем созданный абзац в тело документа.
Пока выполняется каждая операция, ничего больше не может произойти — обработка (отображение) документа приостановлена. Так происходит, как было сказано в предыдущей статье, потому что JavaScript является однопоточным. В каждый момент времени может выполняться только одна команда, обрабатываемая в единственном — главном потоке. Все остальные действия блокируются до окончания выполнения текущей команды.
Так и в примере выше: после нажатия кнопки абзац не сможет появиться пока не будет нажата кнопка OK в окне сообщения. Попробуйте сами:
Примечание: Важно помнить, что alert() , хоть и часто используется для демонстрации синхронных блокирующих операций, сильно не рекомендован к использованию в реальных приложениях.
Асинхронный JavaScript
По причинам, упомянутым ранее (например, относящимся к блокировке), множество Web API особенностей теперь используют асинхронный код, особенно те,что имеют доступ к внешним устройствам или получают от них некоторые ресурсы, такие как получение файла из сети, запрос к базе данных и получение данных из базы, доступ к потоковому видео на веб-камере, просмотр дисплея на гарнитуре виртуальной реальности.
Почему трудно работать, используя синхронный код? Давайте посмотрим на небольшой пример. Когда вы получаете картинку с сервера, вы не можете мгновенно вернуть результат. Это значит что следующий (псевдо) код не сработает:
Это происходит потому что вы не знаете сколько времени займёт загрузка картинки, следовательно, когда вы начнёте выполнять вторую строку кода, сгенерируется ошибка (возможно, периодически, возможно, каждый раз), потому что response ещё не доступен. Вместо этого, ваш код должен дождаться возвращения response до того, как попытается выполнить дальнейшие инструкции.
Есть два типа стиля асинхронного кода, с которыми вы столкнётесь в коде JavaScript, старый метод — колбэки (callbacks) и более новый — промисы (promises). В следующих разделах мы познакомимся с каждым из них.
Асинхронные колбэки
Асинхронные колбэки — это функции, которые определяются как аргументы при вызове функции, которая начнёт выполнение кода на заднем фоне. Когда код на заднем фоне завершает свою работу, он вызывает колбэк-функцию, оповещающую, что работа сделана, либо оповещающую о трудностях в завершении работы. Обратные вызовы — немного устаревшая практика, но они все ещё употребляются в некоторых старомодных, но часто используемых API.
Пример асинхронного колбэка вторым параметром addEventListener() (как мы видели выше):
Первый параметр — тип обрабатываемого события, второй параметр — колбэк-функция, вызываемая при срабатывании события.
При передаче колбэк-функции как аргумента в другую функцию, мы передаём только ссылку на функцию как аргумент, следовательно колбэк-функция не выполняется мгновенно. Она вызывается асинхронно внутри тела, содержащего функцию. Эта функция должна выполнять колбэк-функцию в нужный момент.
Вы можете написать свою собственную функцию, содержащую колбэк-функцию. Давайте взглянем на ещё один пример, в котором происходит загрузка ресурсов через XMLHttpRequest API (запустите пример, и посмотрите исходный код):
Мы создали функцию displayImage() , которая представляет blob, переданный в неё, как объект URL, и создаёт картинку, в которой отображается URL, добавляя её в элемент документа <body> . Однако, далее мы создаём функцию loadAsset() , которая принимает колбэк-функцию в качестве параметра, вместе с URL для получения данных и типом контента. Для получения данных из URL используется XMLHttpRequest (часто сокращается до аббревиатуры «XHR») , перед тем как передать ответ в колбэк-функцию для дальнейшей обработки. В этом случае колбэк-функция ждёт, пока XHR закончит загрузку данных (используя обработчик события onload ) перед отправкой данных в колбэк-функцию.
Колбэк-функции универсальны — они не только позволяют вам контролировать порядок, в котором запускаются функции и данные, передающиеся между ними, они также позволяют передавать данные различным функциям, в зависимости от обстоятельств. Вы можете выполнять различные действия с загруженным ответом, такие как processJSON() , displayText() , и другие.
Заметьте, что не все колбэк-функции асинхронны — некоторые запускаются синхронно. Например, при использовании Array.prototype.forEach() для перебора элементов массива (запустите пример, и посмотрите исходный код):
В этом примере мы перебираем массив с именами греческих богов и выводим индексы и значения в консоль. Ожидаемый параметр для forEach() — это Колбэк-функция, которая содержит два параметра: ссылку на имя массива и значения индексов. Однако эта функция не ожидает никаких действий — она запускается немедленно.
Промисы
Промисы — новый стиль написания асинхронного кода, который используется в современных Web API. Хорошим примером является fetch() API, который современнее и эффективнее чем XMLHttpRequest . Посмотрим на краткий пример, из нашей статьи Fetching data from the server:
Примечание: вы можете посмотреть законченную версию на github (посмотрите исходный код и запустите пример).
В примере видно, как fetch() принимает один параметр — URL ресурса, который нужно получить из сети, — и возвращает промис. Промис — это объект, представляющий асинхронную операцию, выполненную удачно или неудачно. Он представляет собой как бы промежуточное состояние. По сути, это способ браузера сказать: «я обещаю вернуться к вам с ответом как можно скорее», поэтому в дословном переводе «промис» (promise) означает «обещание».
Может понадобиться много времени, чтобы привыкнуть к данной концепции; это немного напоминает Кота Шрёдингера в действии. Ни один из возможных результатов ещё не произошёл, поэтому операция fetch в настоящее время ожидает результата. Далее у нас есть три блока кода следующих сразу после fetch() :
- Два then() блока. Оба включают в себя функцию обратного вызова, которая запустится, если предыдущая операция закончилась успешно, и каждая колбэк-функция принимает на вход результат предыдущей успешно выполненной операции, таким образом вы можете выполнять операции последовательно. Каждый .then() блок возвращает новый promise, это значит что вы можете объединять в цепочки блоки .then() , таким образом можно выполнить несколько асинхронных операций по порядку, одну за другой. блок описывается в конце и будет запущен если какой-либо .then() блок завершится с ошибкой — это аналогично синхронному try. catch , ошибка становится доступной внутри catch() , что может быть использовано для сообщения пользователю о типе возникшей ошибки. Однако синхронный try. catch не будет работать с promise, хотя будет работать с async/await, с которыми вы познакомитесь позже.
Примечание: вы узнаете намного больше о promise позже в этом модуле, так что не волнуйтесь если вы что-нибудь не поняли.
Очередь событий
Асинхронные операции, такие как промисы, помещаются в очередь событий, которая запускается после завершения обработки основного потока, чтобы они не блокировали выполнение JavaScript-кода. Поставленные в очередь операции завершатся как можно скорее, а затем вернут свои результаты в среду JavaScript.
Промисы и колбэк-функции
Промисы имеют некоторое сходство со старомодными колбэк-функциями. По сути, они являются возвращаемым объектом, к которому вы присоединяете колбэк-функции, вместо того, чтобы передавать колбэки в функцию.
Тем не менее, промисы сделаны специально для обработки асинхронных операций, и имеют много преимуществ по сравнению с колбэками:
- Вы можете объединить несколько асинхронных операций вместе, используя несколько операций .then() , передавая результат одного в следующий в качестве входных данных. Это гораздо сложнее сделать с колбэками, которые часто заканчиваются массивным «адом колбэков» (также известным как callback hell).
- Обратные вызовы Promise всегда вызываются в строгом порядке, который они помещают в очередь событий..
- Обработка ошибок намного лучше — все ошибки обрабатываются одним блоком .catch () в конце блока, а не обрабатываются индивидуально на каждом уровне «пирамиды».
- Промисы избегают инверсии управления, в отличие от колбэков, которые теряют полный контроль над тем, как будет выполняться функция при передаче колбэка в стороннюю библиотеку.
Природа асинхронного кода
Давайте рассмотрим пример, который дополнительно иллюстрирует природу асинхронного кода, показывая, что может произойти, когда мы не полностью осознаем порядок выполнения кода, и проблемы, связанные с попыткой трактовать асинхронный код как синхронный. Следующий пример довольно похож на тот, что мы видели раньше. Одно из отличий состоит в том, что мы включили ряд операторов console.log() чтобы проиллюстрировать порядок, в котором, как вы думаете, будет выполняться код.
Браузер начнёт выполнение кода, увидит первый консольный оператор (Starting) и выполнит его, а затем создаст переменную image .
Затем он переместится на следующую строку и начнёт выполнять блок fetch () , но, поскольку fetch () выполняется асинхронно без блокировки, выполнение кода продолжается после кода, связанного с промисом, тем самым достигая окончательного оператора ( All done! ) и выводя его на консоль.
Только после того, как блок fetch () полностью завершит работу и доставит свой результат через блоки .then () , мы наконец увидим второе сообщение console.log () ( It worked 😉 ). Таким образом, сообщения появились не в том порядке, который вы могли ожидать:
- Starting
- All done!
- It worked 🙂
Если вы запутались, рассмотрим следующий небольшой пример:
Этот пример очень схож с предыдущим в своём поведении — первое и третье сообщения console.log () будут показаны немедленно, но второе будет заблокировано, пока кто-то не нажмёт кнопку мыши. Предыдущий пример работает аналогичным образом, за исключением того, что в этом случае второе сообщение блокируется цепочкой промисов, получая ресурс, а затем отображая его на экране, а не щелчком мыши.
В менее простом примере кода такая система может вызвать проблему — вы не можете включить блок асинхронного кода, который возвращает результат, на который вы потом будете полагаться в блоке синхронного кода. Вы просто не можете гарантировать, что асинхронная функция вернётся до того, как браузер обработает синхронный блок.
Чтобы увидеть это в действии, попробуйте взять локальную копию нашего примера и измените третий вызов console.log () следующим образом:
Теперь вместо третьего сообщения должна возникнуть следующая ошибка:
Это происходит потому, что в то же время браузер пытается запустить третий console.log() , блок fetch() ещё не закончил выполнение, поэтому переменная image ещё не имеет значения.
Примечание: Из соображений безопасности вы не можете применять fetch() к файлам из вашей локальной системы (или запустить другие такие операции локально); чтобы запустить локально пример выше вам необходимо запустить его через локальный веб-сервер.
Активное обучение: сделайте все это асинхронно!
Чтобы исправить проблемный пример с fetch() и заставить все три сообщения console.log() появиться в желаемом порядке, вы можете также запустить третье сообщение console.log() асинхронно. Этого можно добиться, переместив его внутрь другого блока .then() присоединённого к концу второго, или просто переместив его внутрь второго блока then() . Попробуйте исправить это сейчас..
Примечание: Если вы застряли, вы можете найти ответ здесь (также можно посмотреть запущенный пример). Также вы можете найти много информации о промисах в нашем гайде Основные понятия асинхронного программирования позднее в этом модуле.
Заключение
В своей основной форме JavaScript является синхронным, блокирующим, однопоточным языком, в котором одновременно может выполняться только одна операция. Но веб-браузеры определяют функции и API, которые позволяют нам регистрировать функции, которые не должны выполняться синхронно, а должны вызываться асинхронно, когда происходит какое-либо событие (время, взаимодействие пользователя с мышью или получение данных по сети, например). Это означает, что вы можете позволить своему коду делать несколько вещей одновременно, не останавливая и не блокируя основной поток.
Будем ли мы запускать код синхронно или асинхронно, будет зависеть от того, что мы пытаемся сделать.
Есть моменты, когда мы хотим, чтобы все загружалось и происходило прямо сейчас. Например, при применении некоторых пользовательских стилей к веб-странице вы хотите, чтобы стили применялись как можно быстрее.
Если мы выполняем операцию, которая требует времени, например, запрос к базе данных и использование полученных результатов для заполнения шаблонов, лучше вытолкнуть это из основного потока и выполнить задачу асинхронно. Со временем вы узнаете, когда имеет смысл выбирать асинхронную технику вместо синхронной.