Async/await
Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.
Асинхронные функции
Начнём с ключевого слова async . Оно ставится перед функцией, вот так:
async function f()
У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.
Например, эта функция возвратит выполненный промис с результатом 1 :
async function f() < return 1; >f().then(alert); // 1
Можно и явно вернуть промис, результат будет одинаковым:
async function f() < return Promise.resolve(1); >f().then(alert); // 1
Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await , которое можно использовать только внутри async -функций.
Await
// работает только внутри async–функций let value = await promise;
Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.
В этом примере промис успешно выполнится через 1 секунду:
async function f() < let promise = new Promise((resolve, reject) => < setTimeout(() =>resolve("готово!"), 1000) >); let result = await promise; // будет ждать, пока промис не выполнится (*) alert(result); // "готово!" > f();
В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».
Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.
По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then .
await нельзя использовать в обычных функциях
Если мы попробуем использовать await внутри функции, объявленной без async , получим синтаксическую ошибку:
Асинхронный JavaScript: как работают колбеки, промисы и async-await
JavaScript позиционирует асинхронное программирование как фичу. Это означает, что если какое-либо действие занимает некоторое время, ваша программа может продолжать выполнять другие действия, пока предыдущее действие завершается. Как только это действие выполнено, ты можешь что-то сделать с результатом. Это будет отличным решением для таких функций, как выборка данных, но это может сбить с толку новичков. В JavaScript у нас есть несколько различных способов справиться с асинхронностью: колбеки, промисы и async-await.
Колбек функции
Функция колбек — это предоставляемая вами функция, которая будет выполняться после завершения асинхронной операции. Давай создадим фейковую функцию для получения пользовательских данных и используем колбек, чтобы что-то сделать с результатом.
Фейковая функция получения данных
Сначала мы создаем фейковуя функция получения данных, которая не принимает колбек. Поскольку fakeData не существует в течение 300 миллисекунд, у нас нет синхронного доступа к ней.
const fetchData = userId => < setTimeout(() =>< const fakeData = < id: userId, name: 'George', >; // Our data fetch resolves // After 300ms. Now what? >, 300); >
Чтобы действительно иметь возможность что-то делать с нашими fakeData, мы можем передать fetchData как ссылку на функцию, которая будет обрабатывать наши данные!
const fetchData = (userId, callback) => < setTimeout(() =>< const fakeData = < id: userId, name: 'George', >; callback(fakeData); >, 300); >;
Давайт создадим базовую функцию колбек и протестируем ее:
const cb = data => < console.log("Here's your data:", data); >; fetchData(5, cb);
Через 300 мс мы увидим следующее:
Асинхронный JavaScript
В этом модуле мы рассмотрим asynchronous JavaScript, почему это важно, и как это поможет эффективно справляться с потенциальной блокировкой операций, таких как получение ресурсов с сервера или запись в файл.
Необходимые знания
Асинхронный JavaScript довольно сложная тема, и мы советуем пройти Первые шаги в JavaScript и Блоки в JavaScript прежде чем начать эту тему.
Примечание: Если вы работаете за компьютером/планшетом/другим устройством где у вас нет возможности создавать собственные файлы, вы можете попробовать(почти все) примеры кода в одном из веб-приложений, таких, как JSBin или Thimble.
Руководства
В этой статье мы кратко расскажем о различиях между синхронным и асинхронным программированием, проблемах, связанных с синхронным JavaScript-ом, и взглянем на различные техники асинхронного программирования, с которыми вы столкнётесь. Покажем как эти техники помогают решать проблемы синхронного JavaScript.
Расскажем о промисах и том, как использовать API на их основе, а также объясним как работает функция с async и оператор await .
Статья о том, как реализовать собственный API на основе промисов.
Воркеры позволяют запускать код в отдельном потоке и не блокировать основной поток, чтобы код в нём оставался отзывчивым. В статье мы перепишем «тяжелую» синхронную функцию с использованием воркера.
Смотрите также
- Асинхронное программирование из фантастической онлайн книги Марина Хавербеке, Выразительный JavaScript.
Введение в асинхронный JavaScript
В этой статье мы кратко остановимся на проблемах, связанных с синхронным Javascript, а также ознакомимся с несколькими асинхронными методами, демонстрирующими как они могут помочь нам подобные проблемы решить.
Необходимое условие: | Базовая компьютерная грамотность, достаточное понимание основ JavaScript. |
---|---|
Цель: | Ознакомиться с тем, что такое асинхронный JavaScript, чем он отличается от синхронного и в каких случаях используется. |
Синхронный JavaScript
Большая часть функциональности, которую мы рассматривали в предыдущих обучающих модулях, является синхронной — вы запускаете какой-то код, а результат возвращается, как только браузер может его вернуть. Давайте рассмотрим простой пример, как это работает:
const btn = document.querySelector("button"); btn.addEventListener("click", () => alert("You clicked me!"); let pElem = document.createElement("p"); pElem.textContent = "This is a newly-added paragraph."; document.body.appendChild(pElem); >);
В этом блоке кода команды выполняются одна за другой:
- Получаем ссылку на элемент , который уже есть в DOM.
- Добавляем к кнопке обработчик события click так, что при нажатии на неё:
- Выводится сообщение alert() .
- После закрытия сообщения создаём элемент (абзац).
- Затем добавляем в абзац текст.
- В конце добавляем созданный абзац в тело документа.
Пока выполняется каждая операция, ничего больше не может произойти — обработка (отображение) документа приостановлена. Так происходит, потому что JavaScript является однопоточным. В каждый момент времени может выполняться только одна команда, обрабатываемая в единственном — главном потоке. Все остальные действия блокируются до окончания выполнения текущей команды.
Так и в примере выше: после нажатия кнопки абзац не сможет появиться пока не будет нажата кнопка OK в окне сообщения. Попробуйте сами:
button>Нажми меняbutton>
Примечание: Важно помнить, что alert() , хоть и часто используется для демонстрации синхронных блокирующих операций, сильно не рекомендован к использованию в реальных приложениях.
Асинхронный JavaScript
По причинам, упомянутым ранее (например, относящимся к блокировке), множество Web API особенностей теперь используют асинхронный код, особенно те,что имеют доступ к внешним устройствам или получают от них некоторые ресурсы, такие как получение файла из сети, запрос к базе данных и получение данных из базы, доступ к потоковому видео на веб-камере, просмотр дисплея на гарнитуре виртуальной реальности.
Почему трудно работать, используя синхронный код? Давайте посмотрим на небольшой пример. Когда вы получаете картинку с сервера, вы не можете мгновенно вернуть результат. Это значит что следующий (псевдо) код не сработает:
let response = fetch("myImage.png"); let blob = response.blob(); // display your image blob in the UI somehow
Это происходит потому что вы не знаете сколько времени займёт загрузка картинки, следовательно, когда вы начнёте выполнять вторую строку кода, сгенерируется ошибка (возможно, периодически, возможно, каждый раз), потому что response ещё не доступен. Вместо этого, ваш код должен дождаться возвращения response до того, как попытается выполнить дальнейшие инструкции.
Есть два типа стиля асинхронного кода, с которыми вы столкнётесь в коде JavaScript, старый метод — колбэки (callbacks) и более новый — промисы (promises). В следующих разделах мы познакомимся с каждым из них.
Асинхронные колбэки
Асинхронные колбэки — это функции, которые определяются как аргументы при вызове функции, которая начнёт выполнение кода на заднем фоне. Когда код на заднем фоне завершает свою работу, он вызывает колбэк-функцию, оповещающую, что работа сделана, либо оповещающую о трудностях в завершении работы. Обратные вызовы — немного устаревшая практика, но они все ещё употребляются в некоторых старомодных, но часто используемых API.
Пример асинхронного колбэка вторым параметром addEventListener() (как мы видели выше):
.addEventListener("click", () => alert("You clicked me!"); let pElem = document.createElement("p"); pElem.textContent = "This is a newly-added paragraph."; document.body.appendChild(pElem); >);
Первый параметр — тип обрабатываемого события, второй параметр — колбэк-функция, вызываемая при срабатывании события.
При передаче колбэк-функции как аргумента в другую функцию, мы передаём только ссылку на функцию как аргумент, следовательно колбэк-функция не выполняется мгновенно. Она вызывается асинхронно внутри тела, содержащего функцию. Эта функция должна выполнять колбэк-функцию в нужный момент.
Вы можете написать свою собственную функцию, содержащую колбэк-функцию. Давайте взглянем на ещё один пример, в котором происходит загрузка ресурсов через XMLHttpRequest API:
function loadAsset(url, type, callback) const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = type; xhr.onload = function () callback(xhr.response); >; xhr.send(); > function displayImage(blob) const objectURL = URL.createObjectURL(blob); const image = document.createElement("img"); image.src = objectURL; document.body.appendChild(image); > loadAsset("coffee.jpg", "blob", displayImage);
Мы создали функцию displayImage() , которая представляет blob, переданный в неё, как объект URL, и создаёт картинку, в которой отображается URL, добавляя её в элемент документа . Однако, далее мы создаём функцию loadAsset() , которая принимает колбэк-функцию в качестве параметра, вместе с URL для получения данных и типом контента. Для получения данных из URL используется XMLHttpRequest (часто сокращается до аббревиатуры «XHR») , перед тем как передать ответ в колбэк-функцию для дальнейшей обработки. В этом случае колбэк-функция ждёт, пока XHR закончит загрузку данных (используя обработчик события onload (en-US) ) перед отправкой данных в колбэк-функцию.
Колбэк-функции универсальны — они не только позволяют вам контролировать порядок, в котором запускаются функции и данные, передающиеся между ними, они также позволяют передавать данные различным функциям, в зависимости от обстоятельств. Вы можете выполнять различные действия с загруженным ответом, такие как processJSON() , displayText() , и другие.
Заметьте, что не все колбэк-функции асинхронны — некоторые запускаются синхронно. Например, при использовании Array.prototype.forEach() для перебора элементов массива (запустите пример, и посмотрите исходный код):
const gods = ["Apollo", "Artemis", "Ares", "Zeus"]; gods.forEach(function (eachName, index) console.log(index + ". " + eachName); >);
В этом примере мы перебираем массив с именами греческих богов и выводим индексы и значения в консоль. Ожидаемый параметр для forEach() — это Колбэк-функция, которая содержит два параметра: ссылку на имя массива и значения индексов. Однако эта функция не ожидает никаких действий — она запускается немедленно.
Промисы
Промисы — новый стиль написания асинхронного кода, который используется в современных Web API. Хорошим примером является fetch() API, который современнее и эффективнее чем XMLHttpRequest . Посмотрим на краткий пример, из нашей статьи Fetching data from the server:
fetch("products.json") .then((response) => if (!response.ok) throw new Error(`HTTP error: $response.status>`); > return response.json(); >) .then((json) => initialize(json)) .catch((err) => console.error(`Fetch problem: $err.message>`));
Примечание: вы можете посмотреть законченную версию на 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_static чтобы проиллюстрировать порядок, в котором, как вы думаете, будет выполняться код.
.log("Starting"); let image; fetch("coffee.jpg") .then((response) => console.log("It worked :)"); return response.blob(); >) .then((myBlob) => const objectURL = URL.createObjectURL(myBlob); image = document.createElement("img"); image.src = objectURL; document.body.appendChild(image); >) .catch((error) => console.log( "There has been a problem with your fetch operation: " + error.message, ); >); console.log("All done!");
Браузер начнёт выполнение кода, увидит первый консольный оператор (Starting) и выполнит его, а затем создаст переменную image .
Затем он переместится на следующую строку и начнёт выполнять блок fetch () , но, поскольку fetch () выполняется асинхронно без блокировки, выполнение кода продолжается после кода, связанного с промисом, тем самым достигая окончательного оператора ( All done! ) и выводя его на консоль.
Только после того, как блок fetch () полностью завершит работу и доставит свой результат через блоки .then () , мы наконец увидим второе сообщение console.log () ( It worked 😉 ). Таким образом, сообщения появились не в том порядке, который вы могли ожидать:
- Starting
- All done!
- It worked 🙂
Если вы запутались, рассмотрим следующий небольшой пример:
.log("registering click handler"); button.addEventListener("click", () => console.log("get click"); >); console.log("all done");
Этот пример очень схож с предыдущим в своём поведении — первое и третье сообщения console.log () будут показаны немедленно, но второе будет заблокировано, пока кто-то не нажмёт кнопку мыши. Предыдущий пример работает аналогичным образом, за исключением того, что в этом случае второе сообщение блокируется цепочкой промисов, получая ресурс, а затем отображая его на экране, а не щелчком мыши.
В менее простом примере кода такая система может вызвать проблему — вы не можете включить блок асинхронного кода, который возвращает результат, на который вы потом будете полагаться в блоке синхронного кода. Вы просто не можете гарантировать, что асинхронная функция вернётся до того, как браузер обработает синхронный блок.
Чтобы увидеть это в действии попробуйте изменить третий вызов console.log () следующим образом:
.log("All done! " + image + "displayed.");
Теперь вместо третьего сообщения должна возникнуть следующая ошибка:
TypeError: image is undefined; can't access its "src" property
Это происходит потому, что в то же время браузер пытается запустить третий console.log() , блок fetch() ещё не закончил выполнение, поэтому переменная image ещё не имеет значения.
Примечание: Из соображений безопасности вы не можете применять fetch() к файлам из вашей локальной системы (или запустить другие такие операции локально); чтобы запустить локально пример выше вам необходимо запустить его через локальный веб-сервер.
Активное обучение: сделайте все это асинхронно!
Чтобы исправить проблемный пример с fetch() и заставить все три сообщения console.log() появиться в желаемом порядке, вы можете также запустить третье сообщение console.log() асинхронно. Этого можно добиться, переместив его внутрь другого блока .then() присоединённого к концу второго, или просто переместив его внутрь второго блока then() . Попробуйте исправить это.
Заключение
В своей основной форме JavaScript является синхронным, блокирующим, однопоточным языком, в котором одновременно может выполняться только одна операция. Но веб-браузеры определяют функции и API, которые позволяют нам регистрировать функции, которые не должны выполняться синхронно, а должны вызываться асинхронно, когда происходит какое-либо событие (время, взаимодействие пользователя с мышью или получение данных по сети, например). Это означает, что вы можете позволить своему коду делать несколько вещей одновременно, не останавливая и не блокируя основной поток.
Будем ли мы запускать код синхронно или асинхронно, будет зависеть от того, что мы пытаемся сделать.
Есть моменты, когда мы хотим, чтобы все загружалось и происходило прямо сейчас. Например, при применении некоторых пользовательских стилей к веб-странице вы хотите, чтобы стили применялись как можно быстрее.
Если мы выполняем операцию, которая требует времени, например, запрос к базе данных и использование полученных результатов для заполнения шаблонов, лучше вытолкнуть это из основного потока и выполнить задачу асинхронно. Со временем вы узнаете, когда имеет смысл выбирать асинхронную технику вместо синхронной.