Асинхронность в 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 — в качестве примера).
Примеры
Можно загрузить стек вызовов так, чтобы он работал бесконечно и следующая функция из очереди вызовов не вызвалась. Попробуйте, например, запустить вот такой код.
document.addEventListener(‘click’, function()< console.log(‘clicked’) >); while(true)
Обработчик клика не сработает, а бесконечный цикл загрузит процессор компьютера. Вкладка зависнет 😉
Клик вызовет «тяжелую» для расчета функцию. После клика в консоль пишется start, в конце выполнения функции — end. Выполнение функции на моем ноутбуке занимает несколько секунд. Все время, пока выполняется функция, квадратик мигает. Это значит, что анимации в CSS выполняются асинхронно JavaScript-коду.
Но что будет, если вместо opacity менять размер?
Квадратик зависнет на время выполнения функции. Дело в том, что CSS-свойство height обращается к DOM. Как мы помним, к DOM можно обращаться только из одного потока, чтобы не было проблем с параллельным доступом.
Делаем вывод, что для анимации лучше пользоваться свойствами, которые не меняют DOM (transform, opacity и т.д.). А всю тяжелую работу в JavaScript лучше делать асинхронно. Например вот так.
Код написан для наглядности и на коленке, в бою применять не рекомендуется. Мы делим большой кусок работы на маленькие и выполняем асинхронно. При этом интерфейс не блокируется. Для таких расчетов можно пользоваться веб-воркерами.
Вывод
Благодаря JavaScript мы пишем асинхронные приложения, не задумываясь о многопоточности: о целостности и непротиворечивости данных. За эти преимущества мы платим огромным числом обратных вызовов, блокированием основного потока и постоянными потерями контекста.
О том, как бороться с последней проблемой, я расскажу в следующий раз.
- асинхронное программирование
- фронтенд-разработка
- javascript
- settimeout
Асинхронный 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 мс мы увидим следующее:
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
В этом модуле мы рассмотрим asynchronous JavaScript, почему это важно, и как это поможет эффективно справляться с потенциальной блокировкой операций, таких как получение ресурсов с сервера или запись в файл.
Необходимые знания
Асинхронный JavaScript довольно сложная тема, и мы советуем пройти Первые шаги в JavaScript и Блоки в JavaScript прежде чем начать эту тему.
Примечание: Если вы работаете за компьютером/планшетом/другим устройством где у вас нет возможности создавать собственные файлы, вы можете попробовать(почти все) примеры кода в одном из веб-приложений, таких, как JSBin или Thimble.
Руководства
В этой статье мы кратко расскажем о различиях между синхронным и асинхронным программированием, проблемах, связанных с синхронным JavaScript-ом, и взглянем на различные техники асинхронного программирования, с которыми вы столкнётесь. Покажем как эти техники помогают решать проблемы синхронного JavaScript.
Расскажем о промисах и том, как использовать API на их основе, а также объясним как работает функция с async и оператор await .
Статья о том, как реализовать собственный API на основе промисов.
Воркеры позволяют запускать код в отдельном потоке и не блокировать основной поток, чтобы код в нём оставался отзывчивым. В статье мы перепишем «тяжелую» синхронную функцию с использованием воркера.
Смотрите также
- Асинхронное программирование из фантастической онлайн книги Марина Хавербеке, Выразительный JavaScript.
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 9 дек. 2023 г. by MDN contributors.
Your blueprint for a better internet.