Что такое асинхронность в javascript

Что такое асинхронность в javascript

Асинхронный JavaScript: как работают колбеки, промисы и async-await

JavaScript позиционирует асинхронное программирование как фичу. Это означает, что если какое-либо действие занимает некоторое время, ваша программа может продолжать выполнять другие действия, пока предыдущее действие завершается. Как только это действие выполнено, ты можешь что-то сделать с результатом. Это будет отличным решением для таких функций, как выборка данных, но это может сбить с толку новичков. В JavaScript у нас есть несколько различных способов справиться с асинхронностью: колбеки, промисы и async-await.

Функция колбек — это предоставляемая вами функция, которая будет выполняться после завершения асинхронной операции. Давай создадим фейковую функцию для получения пользовательских данных и используем колбек, чтобы что-то сделать с результатом.

Сначала мы создаем фейковуя функция получения данных, которая не принимает колбек. Поскольку fakeData не существует в течение 300 миллисекунд, у нас нет синхронного доступа к ней.

Чтобы действительно иметь возможность что-то делать с нашими fakeData, мы можем передать fetchData как ссылку на функцию, которая будет обрабатывать наши данные!

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 :

  1. Нам нужно заменить вызовы .then на await .
  2. И добавить ключевое слово async перед объявлением функции.

Получилось очень просто и читаемо, правда? Гораздо лучше, чем раньше.

Программисты, узнав об await , часто пытаются использовать эту возможность на верхнем уровне вложенности (вне тела функции). Но из-за того, что await работает только внутри async –функций, так сделать не получится:

Можно обернуть этот код в анонимную async –функцию, тогда всё заработает:

Как и promise.then , await позволяет работать с промис–совместимыми объектами. Идея в том, что если у объекта можно вызвать метод then , этого достаточно, чтобы использовать его с await .

В примере ниже, экземпляры класса Thenable будут работать вместе с await :

Когда await получает объект с .then , не являющийся промисом, JavaScript автоматически запускает этот метод, передавая ему аргументы – встроенные функции resolve и reject . Затем await приостановит дальнейшее выполнение кода, пока любая из этих функций не будет вызвана (в примере это строка (*) ). После чего выполнение кода продолжится с результатом resolve или reject соответственно.

Для объявления асинхронного метода достаточно написать async перед именем:

Введение в асинхронный JavaScript

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

Необходимое условие: Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель: Ознакомиться с тем, что такое асинхронный JavaScript, чем он отличается от синхронного и в каких случаях используется.

Синхронный JavaScript

Чтобы (позволить нам) понять что есть асинхронный JavaScript, нам следовало бы для начала убедиться, что мы понимаем что такое синхронный JavaScript. Этот раздел резюмирует некоторую информацию из прошлой статьи.

Большая часть функциональности, которую мы рассматривали в предыдущих обучающих модулях, является синхронной — вы запускаете какой-то код, а результат возвращается, как только браузер может его вернуть. Давайте рассмотрим простой пример ( посмотрите онлайн, как это работает и посмотрите исходный код):

В этом блоке кода команды выполняются одна за другой:

  1. Получаем ссылку на элемент <button> , который уже есть в DOM.
  2. Добавляем к кнопке обработчик события click так, что при нажатии на неё:
    1. Выводится сообщение alert() .
    2. После закрытия сообщения создаём элемент <p> (абзац).
    3. Затем добавляем в абзац текст.
    4. В конце добавляем созданный абзац в тело документа.

    Пока выполняется каждая операция, ничего больше не может произойти — обработка (отображение) документа приостановлена. Так происходит, как было сказано в предыдущей статье, потому что 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() , таким образом можно выполнить несколько асинхронных операций по порядку, одну за другой.
    • catch() блок описывается в конце и будет запущен если какой-либо .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, которые позволяют нам регистрировать функции, которые не должны выполняться синхронно, а должны вызываться асинхронно, когда происходит какое-либо событие (время, взаимодействие пользователя с мышью или получение данных по сети, например). Это означает, что вы можете позволить своему коду делать несколько вещей одновременно, не останавливая и не блокируя основной поток.

    Будем ли мы запускать код синхронно или асинхронно, будет зависеть от того, что мы пытаемся сделать.

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

    Если мы выполняем операцию, которая требует времени, например, запрос к базе данных и использование полученных результатов для заполнения шаблонов, лучше вытолкнуть это из основного потока и выполнить задачу асинхронно. Со временем вы узнаете, когда имеет смысл выбирать асинхронную технику вместо синхронной.

Ссылка на основную публикацию