Управление циклом событий в JavaScript — 1
JavaScript — однопоточный язык, но он не работает в одиночку. За его спиной трудится диспетчер — Event Loop. Именно он решает, какой код выполнить сейчас, а какой — отправить в «очередь ожидания». Узнайте, как устроен этот невидимый менеджер, который помогает писать быстрый и отзывчивый код.
Содержание:
Освойте профессию фронтенд-разработчика и добавьте к нему искусственный интеллект
Узнать большеПонимание работы цикла событий очень важно для профессионального роста в разработчика уровня middle. О циклах событий расскажет Александр Кузьмин — опытный программист с более чем десятью лет работы во frontend-разработке, руководитель отдела клиентских решений в компании IT-Park.
Александр Кузьмин, главный программист и директор отдела разработки для клиентов в компании IT-Park
Как работают асинхронные процессы в JavaScript
Каждый язык программирования использует свои методы для реализации параллельных вычислений. Например, в C++ для этого создают отдельный поток или процесс.
Когда нужно передать задачу «выполни вычисления и сохрани результат, а я позже за ним вернусь», мы сталкиваемся с асинхронностью. Код, инициировавший выполнение, не останавливается в ожидании завершения операции, а продолжает работу. Для ожидания результата многие языки, включая JavaScript, предлагают конструкции async/await, которые позволяют синхронизировать выполнение кода.
Асинхронность — ключевой инструмент JavaScript. До появления Node.js этот язык был основным для клиентской стороны веба (хотя Internet Explorer поддерживал VBScript, он применялся редко). Сегодня сложно представить интернет, где каждый запрос к серверу требовал бы перезагрузки страницы. Вместо этого получили развитие одностраничные приложения (SPA), в которых клиентская часть самостоятельно обрабатывает URL и отображает контент.
Запросы к серверу выполняются асинхронно: после отправки (через XMLHttpRequest или XHR) программа не ждет ответа, а продолжает работу. Когда сервер отвечает, XHR получает сигнал и вызывает заранее указанную функцию обратного вызова (callback).
Если бы JavaScript замирал в ожидании ответа, страница переставала бы реагировать на действия пользователя. Чтобы обеспечить комфортное взаимодействие, операции, требующие ожидания (так называемые блокирующие операции), выносятся за пределы основного потока исполнения. Подробнее о них — во второй части статьи.
При грамотном использовании языковых инструментов последовательное выполнение кода в одном потоке не мешает обрабатывать события. Пользователь может взаимодействовать с интерфейсом без задержек и зависаний.
Цикл событий: механизм управления асинхронными операциями
Для управления порядком выполнения кода в однопоточном JavaScript был создан специальный механизм — цикл событий (Event Loop).
Слово «loop» (петля) точно отражает его суть: это замкнутый цикл, который непрерывно проверяет очередь задач.
Цикл событий управляет порядком выполнения, опираясь на стек вызовов (call stack). Стек формируется при вызове функций. Асинхронные операции (например, callback’ы от таймеров или запросов) попадают в очередь задач. Event Loop следит за стеком: как только он опустеет, цикл берет первую задачу из очереди и помещает ее в стек для выполнения.
Таким образом, в JavaScript существует тесное взаимодействие между синхронным стеком вызовов и асинхронной очередью задач, которой управляет Event Loop.
Для обеспечения согласованности данных каждая функция должна выполняться до конца. Это связано с однопоточностью языка, а также с такими особенностями, как замыкания. Единственный поток реализован как очередь контекстов выполнения, в которую встраиваются задачи, обработанные циклом событий.
В JavaScript важно различать «контекст выполнения» (execution context) и «область видимости» (scope). Контекст выполнения — это более широкое понятие, которое включает в себя scope, переменные, this и другие служебные данные.
Процесс создания контекста выполнения
JavaScript — интерпретируемый язык, код выполняется построчно. Однако этому процессу предшествует фаза создания контекста выполнения.
Когда скрипт начинает выполняться, создается глобальный контекст. На этапе его подготовки формируется так называемый объект переменных (Variable Object, VO). Интерпретатор анализирует («поднимает» или hoists) объявления:
- переменных, объявленных с помощью var (а также let и const, но с иной логикой);
- функций, объявленных через function declaration.
Это формирует VO в рамках текущего контекста. Затем устанавливается связь с внешней областью видимости (scope chain), добавляются параметры функции и их значения.
Важно: место объявления функции (если это function declaration) не имеет значения для фазы создания контекста — она становится доступной до своего фактического местонахождения в коде.
Рассмотрим пример:
javascript
console.log(a); // undefined
var a = 5;
function func() {
console.log(‘Hello‘);
}
Голосование данного скрипта создаст следующий порядок:
1. В фазе создания: в VO заносятся объявление переменной a (со значением undefined) и объявление функции func.
2. В фазе выполнения: выполняется код построчно. console.log(a) выведет undefined, затем переменной a будет присвоено значение 5.
Изменим скрипт, добавив setTimeout:
javascript
setTimeout(() => {
console.log(‘Timeout‘);
}, 0);
console.log(‘Hello‘);
Может показаться, что функция в setTimeout выполнится мгновенно. Однако вывод будет следующим:
1. Hello
2. Timeout
Несмотря на нулевую задержку, callback от setTimeout попадет в очередь задач и будет выполнен только после того, как основной поток (стек вызовов) освободится, то есть после вывода Hello. Это наглядно демонстрирует работу цикла событий.
… Это наглядно демонстрирует работу цикла событий.
Вот что происходит по шагам:
1. Стек. Начинает выполняться синхронный код. setTimeout регистрируется, и его колбэк с нулевой задержкой отправляется не в стек, а в API окружения (браузера или Node.js).
2. Очередь задач. Как только таймер истекает (практически мгновенно), колбэк помещается в очередь макрозадач (Task Queue).
3. Работа Event Loop. Цикл событий постоянно проверяет: «Пуст ли стек?». Пока выполняется console.log(‘Hello’), стек не пуст.
4. Выполнение. Только когда весь синхронный код выполнен и стек очистился, Event Loop берет первую задачу из очереди (наш колбэк) и помещает ее в стек для выполнения.
Таким образом, нулевая задержка в setTimeout не означает «выполнить сейчас», а означает «добавить в очередь задач как можно скорее, после завершения текущего кода». Это ключевой принцип неблокирующего выполнения в JavaScript.
Взаимосвязь фронтенд-разработки и искусственного интеллекта
На курсе вы освоите создание интерфейсов для веб-сервисов, научитесь использовать языки программирования и различные технологии. Вы сможете разрабатывать приложения с разным функционалом: планировщики задач, мессенджеры и интернет-магазины.
Узнать подробнее