Event loop что это
Событийный цикл: микрозадачи и макрозадачи
Поток выполнения в браузере, равно как и в Node.js, основан на событийном цикле.
Понимание работы событийного цикла важно для оптимизаций, иногда для правильной архитектуры.
В этой главе мы сначала разберём теорию, а затем рассмотрим её практическое применение.
Событийный цикл
Идея событийного цикла очень проста. Есть бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.
Общий алгоритм движка:
Это формализация того, что мы наблюдаем, просматривая веб-страницу. Движок JavaScript большую часть времени ничего не делает и работает, только если требуется исполнить скрипт/обработчик или обработать событие.
…Но, возможно, мы хотим что-нибудь показать во время выполнения задачи, например, индикатор выполнения.
Так будет красивее:
Пример 3: делаем что-нибудь после события
В обработчике события мы можем решить отложить некоторые действия, пока событие не «всплывёт» и не будет обработано на всех уровнях. Мы можем добиться этого, обернув код в setTimeout с нулевой задержкой.
Макрозадачи и Микрозадачи
Помимо макрозадач, описанных в этой части, существуют микрозадачи, упомянутые в главе Микрозадачи.
Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач перед тем, как выполнить следующую макрозадачу или отобразить изменения на странице, или сделать что-то ещё.
Какой здесь будет порядок?
Более подробное изображение событийного цикла выглядит так:
Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.
Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.
Итого
Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со спецификацией):
Чтобы добавить в очередь новую макрозадачу:
Этот способ можно использовать для разбиения больших вычислительных задач на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этих частей.
Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).
Для добавления в очередь новой микрозадачи:
События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.
Поэтому queueMicrotask можно использовать для асинхронного выполнения функции в том же состоянии окружения.
Для длительных тяжёлых вычислений, которые не должны блокировать событийный цикл, мы можем использовать Web Workers.
Это способ исполнить код в другом, параллельном потоке.
Web Workers могут обмениваться сообщениями с основным процессом, но они имеют свои переменные и свой событийный цикл.
Web Workers не имеют доступа к DOM, поэтому основное их применение – вычисления. Они позволяют задействовать несколько ядер процессора одновременно.
Задачи
Что код выведет в консоли?
Давайте разберем что здесь происходит.
Изначально в стеке выполнения находится сам скрипт, поэтому сначала выполняется только он.
Параллельная модель и цикл событий.
Параллелизм/Многопоточность в JavaScript работает за счёт цикла событий (event loop), который отвечает за выполнение кода, сбора и обработки событий и выполнения под-задач из очереди (queued sub-tasks). Эта модель весьма отличается от других языков программирования, таких как C и Java.
Концепция жизненного цикла
В следующей секции объясняется теоретическая модель. Современные JavaScript движки внедряют/имплементируют и существенно оптимизируют этот процесс.
Визуальное представление
Для лучшего визуального представления работы Event loop, Вы можете ознакомиться с данным видео: https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=389s
Объекты размещаются в куче. Куча — это просто имя для обозначения большой неструктурированной области памяти.
Очередь
Среда выполнения JavaScript содержит очередь задач. Эта очередь — список задач, подлежащих обработке. Каждая задача ассоциируется с некоторой функцией, которая будет вызвана, чтобы обработать эту задачу.
Когда стек полностью освобождается, самая первая задача извлекается из очереди и обрабатывается. Обработка задачи состоит в вызове ассоциированной с ней функции с параметрами, записанными в этой задаче. Как обычно, вызов функции создаёт новый контекст выполнения и заносится в стек вызовов.
Обработка задачи заканчивается, когда стек снова становится пустым. Следующая задача извлекается из очереди и начинается её обработка.
Цикл событий
Модель событийного цикла ( event loop ) называется так потому, что отслеживает новые события в цикле:
queue.waitForMessage ожидает поступления задач, если очередь пуста.
Запуск до завершения
Каждая задача выполняется полностью, прежде чем начнёт обрабатываться следующая. Благодаря этому мы точно знаем: когда выполняется текущая функция – она не может быть приостановлена и будет целиком завершена до начала выполнения другого кода (который может изменить данные, с которыми работает текущая функция). Это отличает JavaScript от такого языка программирования как C. Поскольку в С функция, запущенная в отдельном потоке, в любой момент может быть остановлена, чтобы выполнить какой-то другой код в другом потоке.
У данного подхода есть и минусы. Если задача занимает слишком много времени, то веб-приложение не может обрабатывать действия пользователя в это время (например, скролл или клик). Браузер старается смягчить проблему и выводит сообщение «скрипт выполняется слишком долго» («a script is taking too long to run») и предлагает остановить его. Хорошей практикой является создание задач, которые исполняются быстро, и если возможно, разбиение одной задачи на несколько мелких.
Добавление событий в очередь
Вызов setTimeout добавит событие в очередь по прошествии времени, указанного во втором аргументе вызова. Если очередь событий на тот момент будет пуста, то событие обработается сразу же, в противном случае событию функции setTimeout придётся ожидать завершения обработки остальных событий в очереди. Именно поэтому второй аргумент setTimeout корректно считать не временем, через которое выполнится функция из первого аргумента, а минимальное время, через которое она сможет выполниться.
Нулевые задержки
Нулевая задержка не даёт гарантии, что обработчик выполнится через ноль миллисекунд. Вызов setTimeout с аргументом 0 (ноль) не завершится за указанное время. Выполнение зависит от количества ожидающих задач в очереди. Например, сообщение »this is just a message» из примера ниже будет выведено на консоль раньше, чем произойдёт выполнение обработчика cb1. Это произойдёт, потому что задержка – это минимальное время, которое требуется среде выполнения на обработку запроса.
Связь нескольких потоков между собой
Web Worker или кросс-доменный фрейм имеют свой собственный стек, кучу и очередь событий. Два отдельных событийных потока могут связываться друг с другом, только через отправку сообщений с помощью метода postMessage. Этот метод добавляет сообщение в очередь другого, если он конечно принимает их.
Никогда не блокируется
Очень интересное свойство цикла событий в JavaScript, что в отличие от множества других языков, поток выполнения никогда не блокируется. Обработка I/O обычно осуществляется с помощью событий и колбэк-функций, поэтому даже когда приложение ожидает запрос от IndexedDB или ответ от XHR, оно может обрабатывать другие процессы, например пользовательский ввод.
Существуют хорошо известные исключения как alert или синхронный XHR, но считается хорошей практикой избегать их использования.
Как устроен Event Loop в JavaScript: параллельная модель и цикл событий
В Event Loop в языке JavaScript заключается секрет асинхронного программирования. Сам по себе JS является однопоточным, но при использовании нескольких умных структур данных можно создать иллюзию многопоточности (параллельная модель). Как это происходит, расскажем в этой статье.
Код JavaScript работает только в однопоточном режиме. Это означает, что в один и тот же момент может происходить только одно событие. С одной стороны это хорошо, так как такое ограничение значительно упрощает процесс программирования, здесь не возникает проблем параллелизма. Но, как правило, в большинстве браузеров в каждой из вкладок существует свой цикл событий. Среда управляет несколькими параллельными циклами.
Общим знаменателем для всех сред является встроенный механизм, называемый Event Loop JavaScript, который обрабатывает выполнение нескольких фрагментов программы, вызывая каждый раз движок JS.
Какова идея цикла событий?
Существует бесконечный цикл событий, в котором JavaScript движок ожидает свою задачу, выполняет ее и ждет новую. Алгоритм работы движка мы можем видеть при просмотре любой веб-страницы. Он включается в работу тогда, когда необходимо обработать какое-либо событие или скрипт. Схема работы выглядит следующим образом:
Визуально процесс можно изобразить так:
Отслеживание новых событий в цикле:
Если в очереди нет задач, queue.waitForMessage ожидает их поступления.
Как события добавляются в очередь
Все события в браузерах постоянно добавляются в очередь, если они произошли или имеют свой обработчик. setTimeout может добавлять событие в очередь не сразу, а по прошествии указанного времени. Если на данный момент в очереди нет событий, то оно поступит в обработку сразу.
Когда операция setTimeout обрабатывается в стеке, она отправляется соответствующему API, который ожидает до указанного времени, чтобы отправить эту операцию в обработку. Среда управляет несколькими параллельными циклами событий, например, для обработки вызовов API. Веб-воркеры также работают в собственном цикле событий.
Операция отправляется в очередь событий. Следовательно, у нас есть циклическая схема для выполнения асинхронных операций в JavaScript. Сам язык является однопоточным, но API-интерфейсы браузера действуют как отдельные потоки.
Цикл событий постоянно проверяет, пуст ли стек вызовов. Если он пуст, новые функции добавляются из очереди событий. Если это не так, то выполняется текущий вызов функции.
Давайте посмотрим, как отложить выполнение функции до тех пор, пока стек не очистится.
Пример использования setTimeout(() => <>), 0) заключается в том, чтобы вызвать функцию, но выполнить ее после выполнения всех остальных функций в коде.
bar, baz, foo — случайные имена.
При запуске кода сначала вызывается foo(). Внутри foo() мы сначала вызываем setTimeout, передавая bar в качестве аргумента, и инструктируем его таким образом, чтобы он запускался как можно быстрее, передавая 0 в качестве таймера. Затем мы вызываем baz().
Порядок функций в программе:
Почему так происходит?
Очередь событий
При вызове setTimeout(), браузер или Node.js запускают таймер. По истечении таймера (в нашем случае мы установили 0) в качестве тайм-аута, функция обратного вызова помещается в очередь событий.
Очередь событий также является местом, где инициированные пользователем события (клики мышью, ввод с клавиатуры и др.) помещаются в очередь до того, как код сможет на них отреагировать.
Event Loop отдает приоритет стеку вызовов. Сначала он обрабатывает все, что находит в стеке вызовов, а когда там ничего не остается, переходит к обработке очереди событий.
setTimeout с аргументом 0 не гарантирует, что обработка будет выполнена мгновенно. Все зависит от того, сколько задач в данный момент находится в очереди. В примере ниже ”message” будет выведена быстрее обработчика callback_1. Объясняется это тем, что задержка представляет собой минимальное время, необходимое среде на выполнение запроса.
Цикл событий в JavaScript отличается от других языков тем, что его поток выполнения никогда не блокируется, кроме некоторых исключений, таких как alert или синхронный HTTP-запрос, которые не рекомендуется использовать. Поэтому даже когда приложение ожидает запросы из хранилища или ответ с сервера, оно может обрабатывать другие процессы, например пользовательский ввод.
В заключение
Веб-сайты стали более интерактивными и динамичными, необходимость выполнения интенсивных операций стала все более актуальной (к примеру, выполнение внешних сетевых запросов для получения данных API). Чтобы обрабатывать эти операции, необходимо использование методов асинхронного программирования. Встроенный механизм Event Loop помогает JavaScript обрабатывать асинхронный код.
Простой метод измерения реальной скорости загрузки страниц у посетителей сайта
Что делать, если часть логики написана на PHP, а часть на NodeJS
Как можно закэшировать данные и выиграть в производительности
Как работает Server-Sent API с примерами
Примеры применения Javascript в Nginx’e
Как просто сделать удобный дебаг и не лазить в код или как бородатые хакеры перехватывают ajax-запросы, нарушая вашу безопасность.
В своем блоге индийский разработчик Шашват Верма (Shashwat Verma) рассказал, как преобразовать веб-сайт или веб-страницу в прогрессивное веб-приложение (PWA).
Event loop¶
Event loop (или цикл событий) позволяет выполнять однопоточному Node.js неблокирующие операции ввода/вывода, передавая их выполнение ядру системы, когда это возможно.
Стадии event loop¶
Кратко работу event loop Node.js можно описать так: операция передается на выполнения ядру системы, после завершения Node.js получает уведомление в том, что определенная для операции callback-функция может быть добавлена в очередь выполнения.
Инициализация event loop происходит в момент запуска сервера Node.js, и с этого момента он начинает свою работу, которую можно разделить на несколько этапов:
timers выполнение callback-функций, зарегистрированных функциями setTimeout() и setInterval() ; pending callbacks вызов callback-функций операций ввода/вывода, выполнение которых было отложено на предыдущей стадии цикла событий; idle, prepare выполнение внутренних действий, необходимых самому event loop; poll выполнение callback-функций завершенных асинхронных операций и управление фазой timers; check выполнение callback-функций, зарегистрированных функцией setImmediate() ; close callbacks обработка внезапно завершающихся действий.
На стадии timers выполняются зарегистрированные таймерами функции, причем переход на стадию контролируется стадией poll. Из-за блокировки стадией poll цикла событий, таймеры могут выполняться с некоторой задержкой, т. е. через больший интервал времени, чем тот, который был задан. Рассмотрим ситуацию на примере.
Результат работы кода.
При pending callbacks выполняются действия, отложенные на предыдущей итерации event loop. Например, это могут быть сообщения об ошибках, которые не были выведены ранее из-за попыткеи системы их исправить.
При переходе в фазу poll в первую очередь проверяется, сформировалась ли очередь из callback-функций выполненных асинхронных действий. Если очередь не пуста, то в синхронном порядке начинается выполнение всех функций, находящихся в очереди. Выполнение будет продолжаться до тех пор, пока очередь не опустеет или не будет достигнут лимит выполняемых за раз callback-функций.
Для недопущения длительной блокировки event loop, в Node.js имеется ограничение на количество выполняемых на стадии poll callback-функций.
На стадии close callbacks вызываются функции, зарегистрированные для действий, возникающих внезапно. например, событие close или disconnect для сокет соединения.
Как управлять event loop в JavaScript. Часть 1
Event loop (событийные циклы) — важная часть архитектуры JavaScript. Мы попросили эксперта объяснить, как в этом разобраться.
Понимание работы event loop — неотъемлемый пункт карьерного роста middle-разработчика. Если хотите знать всё о JavaScript, ждём вас на курсе «Frontend-разработчик». Вы научитесь верстать сайты и создавать интерфейсы, а на выходе получите два готовых проекта в портфолио.
А сейчас о событийных циклах рассказывает Александр Кузьмин — ведущий программист с десятилетним опытом во frontend, руководитель отдела клиентской разработки компании IT-Park. Передаём слово эксперту.
Автор в сфере IT, digital, экономики и финансов. Ведет некоммерческий проект для начинающих писателей «ЛитЦех».
Асинхронность в JavaScript
У каждого языка свой подход к параллельному вычислению данных. Например, в языках типа C++ оно передаётся в отдельный поток или даже процесс, который выполняется на другой машине.
ведущий программист, руководитель отдела клиентской разработки компании IT-Park
Если нужно сообщить потоку что-то вроде «посчитай вот это и положи результат в базу данных, а я когда-нибудь приду за ними», мы имеем дело с асинхронными операциями.
Это значит, что код, который их вызвал, не ждёт завершения выполнения, а продолжает исполняться дальше. Если же мы хотим дождаться результата, у многих современных языков есть операторы async и await для синхронизации исполняемого кода.
В JavaScript асинхронность — основной инструмент. Во времена до появления Node.JS он был практически единственным языком исполнения сценариев на клиенте в вебе (Internet Explorer поддерживал VB Script, но его никто не использовал). Сейчас невозможно представить интернет, где все запросы на сервер отправлялись бы с перезагрузкой страницы. Напротив, мы пришли к одностраничному вебу, в котором на стороне клиента происходит разрешение адресов страниц и отображение соответствующего контента.
Любые данные от сервера запрашиваются асинхронно: отправляется запрос (XMLHttpRequest или XHR), и код не ждёт его возвращения, продолжая выполняться. Когда же сервер отвечает, объект XHR получает уведомление об этом и запускает функцию обратного вызова — callback, который передали в него перед отправкой запроса.
Если придётся ждать, пока запрос придёт, JavaScript перестанет принимать любые события, а страница зависнет. Чтобы пользователь спокойно использовал веб-приложение, запрос выводят из текущего контекста выполнения. Операции, результата которых приходится ждать, прежде чем продолжать выполнение кода, называются блокирующими. О них — во второй части статьи.
Суть кроется в устройстве языка:
JavaScript ориентирован на общение с пользователем, чтобы тот мог запускать несколько событий одновременно.
Если правильно использовать инструменты языка, то выполнение кода, которое происходит последовательно и в одном потоке, никак не мешает приёму событий и реакции на них — человек спокойно работает с интерфейсом, не замечая лагов, сбоев и зависаний.
Event loop в JavaScript — менеджер асинхронных вызовов
Чтобы этот хитрый процесс слаженно работал, в JavaScript реализован механизм для управления очерёдностью исполнения кода. Поскольку это однопоточный язык, возникла необходимость «вклиниваться» в текущий контекст исполнения. Этот механизм называется event loop — событийный цикл.
С английского loop переводится как «петля», что отлично отражает смысл: мы имеем дело с закольцованной очередью.
Event loop регулирует последовательность исполнения контекстов — стек. Он формируется, когда сработало событие или была вызвана функция. Реакция на событие помещается в очередь исполнения, в event loop, который последовательно, с каждым циклом выполняет попадающий в него код. При этом привязанная к событию функция вызывается следующей после текущего контекста исполнения.
В JavaScript постоянно работают связанные между собой синхронная и асинхронная очереди выполнения. Синхронная — стек — формирует очередь и пробрасывает в асинхронную — event loop — вызовы функций, которые будут выполнены после текущего запланированного исполняемого контекста.
Чтобы данные находились в консистентном состоянии, каждая функция должна быть выполнена до конца. Это обусловлено однопоточностью JavaScript и некоторыми другими особенностями, например характерными для функциональных языков программирования замыканиями. Поэтому единственный поток представлен в виде очереди контекстов исполнения, в которой и происходит «вклинивание» функций, прошедших через цикл событий.
Справка
В JavaScript существует понятие «контекст функции». Но есть и другой термин — «контекст исполнения». Это тело функции со всеми переменными и другими функциями, которое называют «область видимости», с английского — “scope”. Важно не путать понятия, это принципиально разные вещи.
Как формируется контекст исполнения
JavaScript — интерпретируемый язык. Это значит, что любой код проходит через интерпретатор, который исполняет его построчно. Но и здесь есть нюансы.
Как только скрипт попадает в интерпретатор, формируются глобальный контекст и глобальная область видимости, в которой держится Variable Object, или VO — объект переменных.
Он формируется из переменных вида Function Declaration и атрибутов функции по следующему принципу. Интерпретатор считывает код и находит все объявления:
Это складывается в VO текущего контекста исполнения. Затем берётся Variable Object внешней области видимости и к нему добавляется сформированный выше VO. Сверху он дополняется параметрами функции и их значениями на момент исполнения.
При этом нет разницы, в каком месте функции они определяются. Переменная может быть определена в любой части кода, как и функция.
VO этого скрипта формируется:
Затем скрипт начнет исполняться по следующему сценарию:
Теперь перепишем скрипт, добавив setTimeout с нулевым тайм-аутом у вызова функции:
На первый взгляд может показаться, что ничего не изменится и функция func будет выполнена без задержки. Но это не так. На самом деле произойдёт следующее:
Почему так происходит?
Всему виной setTimeout, очевидно. Он выводит контекст исполнения функции из синхронного потока, помещая его в event loop. То же самое происходит и с регистрацией событий. Мы можем подписаться на событие при помощи функции addEventListener. Передавая функцию обратного вызова — callback, добавляем её в список функций, которые должны быть вызваны при срабатывании этого события.
Допустим, мы хотим нажатием на кнопку перекрасить её в красный цвет. Код, который это выполняет, выглядит так:
Переданная функция всегда выполняется через event loop. При возникновении события в цикл последовательно попадут все привязанные к нему функции. Для каждой будет формироваться контекст исполнения, который будет запущен следом за текущим.
Если в процессе будет вызвано ещё одно событие, его коллбэки будут вставать в очередь по тому же принципу, возможно, перемежаясь с контекстами исполнения первого события.
Более сложный пример: есть две кнопки, первая перекрашивает фон страницы в красный цвет, а вторая — в жёлтый, но у второй перекрашивание фона завёрнуто в setTimeout с нулевой задержкой. И мы вручную вызываем событие нажатия сначала на жёлтую кнопку, а потом — на красную.
Что происходит в такой ситуации, исходя из того, что мы рассмотрели выше?
Обратите внимание, что исполнение коллбэков событий click на кнопках при вызове из кода происходит сразу же, не попадая в event loop: setTimeout с нулевой задержкой отложил перекраску фона в жёлтый, но функция сама была исполнена в момент вызова.
Это происходит из-за того, что события из кода не требуется выполнять асинхронно. Действительно, в такой ситуации мы находимся в предсказуемом окружении, тогда как пользовательские события могут случаться в любой момент.
Это приводит к теме следующей части: об управлении событийным циклом и тем, что будет в него попадать. Подробно рассмотрим, как грамотно формировать цепочки последовательных асинхронно вызванных контекстов вызова, уменьшая вычислительную сложность каждого из них, освобождая поток и позволяя пользовательским событиям «вклиниваться» в очередь исполнения.
Если вы хотите знать все о JavaScript, ждем вас на курсе «Профессия frontend-разработчик». Вы научитесь верстать сайты и создавать интерфейсы, а на выходе получите два готовых проекта в портфолио.