Как устроен Event Loop в JavaScript: параллельная модель и цикл событий
В Event Loop в языке JavaScript заключается секрет асинхронного программирования. Сам по себе JS является однопоточным, но при использовании нескольких умных структур данных можно создать иллюзию многопоточности (параллельная модель). Как это происходит, расскажем в этой статье.
Код JavaScript работает только в однопоточном режиме. Это означает, что в один и тот же момент может происходить только одно событие. С одной стороны это хорошо, так как такое ограничение значительно упрощает процесс программирования, здесь не возникает проблем параллелизма. Но, как правило, в большинстве браузеров в каждой из вкладок существует свой цикл событий. Среда управляет несколькими параллельными циклами.
Общим знаменателем для всех сред является встроенный механизм, называемый Event Loop JavaScript, который обрабатывает выполнение нескольких фрагментов программы, вызывая каждый раз движок JS.
Какова идея цикла событий?
Существует бесконечный цикл событий, в котором JavaScript движок ожидает свою задачу, выполняет ее и ждет новую. Алгоритм работы движка мы можем видеть при просмотре любой веб-страницы. Он включается в работу тогда, когда необходимо обработать какое-либо событие или скрипт. Схема работы выглядит следующим образом:
- JavaScript бездействует и ждет свою задачу.
- Как только задачи появляются, движок начинает их выполнение, начиная с первой поступившей.
- Если поступила новая задача, но движок занят выполнением предыдущей — она ставится в очередь.
Визуально процесс можно изобразить так:
- Stack (Стек). Представляет собой поток выполнения кода JavaScript. Event Loop выполняет одну простую задачу — осуществляет контроль стека вызовов и очереди обратных вызовов. Если стек вызовов пуст, цикл событий возьмет первое событие из очереди и отправит его в стек вызовов, который его запустит. При вызове нового метода вверху стека выделяется отдельный блок памяти. Стек вызовов отвечает за отслеживание всех операций в очереди, которые должны быть выполнены. При завершении очереди она извлекается из стека.
- Heap (Куча). В куче происходит создание нового объекта.
- Queue (Очередь). Очередь событий отвечает за отправку новых функций на трек обработки. Он следует структуре данных очереди, чтобы поддерживать правильную последовательность, в которой все операции должны отправляться на выполнение. Если проще, то это и есть список задач, которые должны отправиться на обработку и ждут своего часа.
- Web API. Не являются частью JavaScript, они скорее созданы на основе JS. Каждый раз, когда вызывается асинхронная функция, она отправляется в API браузера. На основе команды, полученной из стека вызовов, API запускает собственную однопоточную операцию.
Отслеживание новых событий в цикле:
while(queue.waitForMessage()){ queue.processNextMessage(); }
Если в очереди нет задач, queue.waitForMessage
ожидает их поступления.
Обратите внимание на курсы от наших друзей, школы Hillel и Powercode. Грамотно составленная программа обучения, а также менторство, помогут начинающим разработчикам разобраться во всех деталях и тонкостях.
Как события добавляются в очередь
Все события в браузерах постоянно добавляются в очередь, если они произошли или имеют свой обработчик. setTimeout может добавлять событие в очередь не сразу, а по прошествии указанного времени. Если на данный момент в очереди нет событий, то оно поступит в обработку сразу.
Когда операция setTimeout обрабатывается в стеке, она отправляется соответствующему API, который ожидает до указанного времени, чтобы отправить эту операцию в обработку. Среда управляет несколькими параллельными циклами событий, например, для обработки вызовов API. Веб-воркеры также работают в собственном цикле событий.
Операция отправляется в очередь событий. Следовательно, у нас есть циклическая схема для выполнения асинхронных операций в JavaScript. Сам язык является однопоточным, но API-интерфейсы браузера действуют как отдельные потоки.
Цикл событий постоянно проверяет, пуст ли стек вызовов. Если он пуст, новые функции добавляются из очереди событий. Если это не так, то выполняется текущий вызов функции.
Давайте посмотрим, как отложить выполнение функции до тех пор, пока стек не очистится.
Пример использования setTimeout(() => {}), 0)
заключается в том, чтобы вызвать функцию, но выполнить ее после выполнения всех остальных функций в коде.
Пример:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo()
bar, baz, foo — случайные имена.
При запуске кода сначала вызывается foo(). Внутри foo() мы сначала вызываем setTimeout, передавая bar в качестве аргумента, и инструктируем его таким образом, чтобы он запускался как можно быстрее, передавая 0 в качестве таймера. Затем мы вызываем baz().
Порядок функций в программе:
Почему так происходит?
Очередь событий
При вызове setTimeout(), браузер или Node.js запускают таймер. По истечении таймера (в нашем случае мы установили 0) в качестве тайм-аута, функция обратного вызова помещается в очередь событий.
Очередь событий также является местом, где инициированные пользователем события (клики мышью, ввод с клавиатуры и др.) помещаются в очередь до того, как код сможет на них отреагировать.
Event Loop отдает приоритет стеку вызовов. Сначала он обрабатывает все, что находит в стеке вызовов, а когда там ничего не остается, переходит к обработке очереди событий.
setTimeout с аргументом 0 не гарантирует, что обработка будет выполнена мгновенно. Все зависит от того, сколько задач в данный момент находится в очереди. В примере ниже ”message” будет выведена быстрее обработчика callback_1. Объясняется это тем, что задержка представляет собой минимальное время, необходимое среде на выполнение запроса.
(function () { console.log('start'); setTimeout(function callback() { console.log('message from callback'); }); console.log('message'); setTimeout(function callback_1() { console.log('message from callback_1'); }, 0); console.log('finish'); })(); // "start" // "message" // "finish" // "message from callback" // "message from callback_1"
Цикл событий в JavaScript отличается от других языков тем, что его поток выполнения никогда не блокируется, кроме некоторых исключений, таких как alert или синхронный HTTP-запрос, которые не рекомендуется использовать. Поэтому даже когда приложение ожидает запросы из хранилища или ответ с сервера, оно может обрабатывать другие процессы, например пользовательский ввод.
В заключение
Веб-сайты стали более интерактивными и динамичными, необходимость выполнения интенсивных операций стала все более актуальной (к примеру, выполнение внешних сетевых запросов для получения данных API). Чтобы обрабатывать эти операции, необходимо использование методов асинхронного программирования. Встроенный механизм Event Loop помогает JavaScript обрабатывать асинхронный код. Чтобы выйти на хороший уровень разработки с JavaScript потребуется не только ваше желание, но и наставничество практикующих специалистов. Наши друзья из школы Mate Academy с радостью помогут вам прокачать свои навыки и знания.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: