Что такое event loop
Перейти к содержимому

Что такое event loop

  • автор:

Как работает event loop в javascript

С помощью механизма Event Loop (Цикл событий) становится возможным выполнять асинхронный код в JavaScript.

Event Loop — это специальный механизм на уровне движка js, который координирует работу трёх сущностей: Call Stack (стэк вызовов), Web API (API, предоставляемый браузером), Callback Queue (очередь колбэков).

Работают они следующим образом: движок js анализирует код. Когда он встречает вызов какой-то функции, он перемещает эту функцию в Call Stack. Если эта функция синхронная (например, console.log() ), то она сразу же исполняется, покидает стэк и на её место приходит следующая функция. Если же эта функция асинхронная, например, setTimeout() , обработчик событий, сетевой запрос и т.д., то на помощь приходит браузер со своим Web API (мы же помним, что JavaScript — это однопоточный язык, и сам работать в многопоточном режиме он не может). Event Loop перемещает колбэк асинхронной функции в Web API, а сама асинхронная функция уходит из стэка вызовов. То есть, пока колбэк асинхронной функции находится под управлением Web API, движок js продолжает выполнять другие операции!

Что же происходит с колбэком? В случае, например, setTimeout() , Web API ожидает истечения указанного времени, затем Event Loop перемещает этот колбэк в Callback Queue (очередь колбэков). Когда стэк вызовов освобождается, Event Loop перемещает в него наш колбэк из очереди колбэков, после чего колбэк наконец исполняется и покидает стэк вызовов.

Этот процесс повторяется до тех пор, пока весь js код не будет выполнен.

Здесь представлен наглядный пример работы Event Loop, очень советую ознакомиться!

01 октября 2022

Код JavaScript работает только в однопоточном режиме. Это означает, что в один и тот же момент может происходить только одно событие. С одной стороны это хорошо, так как такое ограничение значительно упрощает процесс программирования, здесь не возникает проблем параллелизма. Но, как правило, в большинстве браузеров в каждой из вкладок существует свой цикл событий. Среда управляет несколькими параллельными циклами. Общим знаменателем для всех сред является встроенный механизм, называемый Event Loop (Цикл событий) JavaScript, который обрабатывает выполнение нескольких фрагментов программы, вызывая каждый раз движок JS. Цикл событий — ключ к асинхронному программированию на JavaScript. Подробнее.

14 января 2023

Хотел бы еще добавить. В ES6 вместе с промисами появилось понятие очередь микротасков. Эта очередь используется промисами и обладает более высоким приоритетом, по сравнению с очередью макротасков (Например setTimeout или SetInterval). Это означает, что промисы будут выполняться раньше, чем вызовы из обычной очереди колбеков.

 , 0); setTimeout(() => , 0); new Promise((resolve, reject) => ) .then(res => console.log(res)); new Promise((resolve, reject) => ) .then(res => console.log(res)); //Вывод Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2 

Событийный цикл: микрозадачи и макрозадачи

Поток выполнения в браузере, равно как и в Node.js, основан на событийном цикле.

Понимание работы событийного цикла важно для оптимизаций, иногда для правильной архитектуры.

В этой главе мы сначала разберём теорию, а затем рассмотрим её практическое применение.

Событийный цикл

Идея событийного цикла очень проста. Есть бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.

Общий алгоритм движка:

  1. Пока есть задачи:
    • выполнить их, начиная с самой старой
  2. Бездействовать до появления новой задачи, а затем перейти к пункту 1

Это формализация того, что мы наблюдаем, просматривая веб-страницу. Движок JavaScript большую часть времени ничего не делает и работает, только если требуется исполнить скрипт/обработчик или обработать событие.

  • Когда загружается внешний скрипт , то задача – это выполнение этого скрипта.
  • Когда пользователь двигает мышь, задача – сгенерировать событие mousemove и выполнить его обработчики.
  • Когда истечёт таймер, установленный с помощью setTimeout(func, . ) , задача – это выполнение функции func
  • И так далее.

Задачи поступают на выполнение – движок выполняет их – затем ожидает новые задачи (во время ожидания практически не нагружая процессор компьютера)

Может так случиться, что задача поступает, когда движок занят чем-то другим, тогда она ставится в очередь.

Очередь, которую формируют такие задачи, называют «очередью макрозадач» (macrotask queue, термин v8).

Например, когда движок занят выполнением скрипта, пользователь может передвинуть мышь, тем самым вызвав появление события mousemove , или может истечь таймер, установленный setTimeout , и т.п. Эти задачи формируют очередь, как показано на иллюстрации выше.

Задачи из очереди исполняются по правилу «первым пришёл – первым ушёл». Когда браузер заканчивает выполнение скрипта, он обрабатывает событие mousemove , затем выполняет обработчик, заданный setTimeout , и так далее.

Пока что всё просто, не правда ли?

Отметим две детали:

  1. Рендеринг (отрисовка страницы) никогда не происходит во время выполнения задачи движком. Не имеет значения, сколь долго выполняется задача. Изменения в DOM отрисовываются только после того, как задача выполнена.
  2. Если задача выполняется очень долго, то браузер не может выполнять другие задачи, обрабатывать пользовательские события, поэтому спустя некоторое время браузер предлагает «убить» долго выполняющуюся задачу. Такое возможно, когда в скрипте много сложных вычислений или ошибка, ведущая к бесконечному циклу.

Это была теория. Теперь давайте взглянем, как можно применить эти знания.

Пример 1: разбиение «тяжёлой» задачи.

Допустим, у нас есть задача, требующая значительных ресурсов процессора.

Например, подсветка синтаксиса (используется для выделения цветом участков кода на этой странице) – довольно процессороёмкая задача. Для подсветки кода надо выполнить синтаксический анализ, создать много элементов для цветового выделения, добавить их в документ – для большого текста это требует значительных ресурсов.

Пока движок занят подсветкой синтаксиса, он не может делать ничего, связанного с DOM, не может обрабатывать пользовательские события и т.д. Возможно даже «подвисание» браузера, что совершенно неприемлемо.

Мы можем избежать этого, разбив задачу на части. Сделать подсветку для первых 100 строк, затем запланировать setTimeout (с нулевой задержкой) для разметки следующих 100 строк и т.д.

Чтобы продемонстрировать такой подход, давайте будем использовать для простоты функцию, которая считает от 1 до 1000000000 .

Если вы запустите код ниже, движок «зависнет» на некоторое время. Для серверного JS это будет явно заметно, а если вы будете выполнять этот код в браузере, то попробуйте понажимать другие кнопки на странице – вы заметите, что никакие другие события не обрабатываются до завершения функции счёта.

let i = 0; let start = Date.now(); function count() < // делаем тяжёлую работу for (let j = 0; j < 1e9; j++) < i++; >alert("Done in " + (Date.now() - start) + 'ms'); > count();

Браузер может даже показать сообщение «скрипт выполняется слишком долго».

Давайте разобьём задачу на части, воспользовавшись вложенным setTimeout :

let i = 0; let start = Date.now(); function count() < // делаем часть тяжёлой работы (*) do < i++; >while (i % 1e6 != 0); if (i == 1e9) < alert("Done in " + (Date.now() - start) + 'ms'); >else < setTimeout(count); // планируем новый вызов (**) >> count();

Теперь интерфейс браузера полностью работоспособен во время выполнения «счёта».

Один вызов count делает часть работы (*) , а затем, если необходимо, планирует свой очередной запуск (**) :

  1. Первое выполнение производит счёт: i=1…1000000.
  2. Второе выполнение производит счёт: i=1000001…2000000.
  3. …и так далее.

Теперь если новая сторонняя задача (например, событие onclick ) появляется, пока движок занят выполнением 1-й части, то она становится в очередь, и затем выполняется, когда 1-я часть завершена, перед следующей частью. Периодические возвраты в событийный цикл между запусками count дают движку достаточно «воздуха», чтобы сделать что-то ещё, отреагировать на действия пользователя.

Отметим, что оба варианта – с разбиением задачи с помощью setTimeout и без – сопоставимы по скорости выполнения. Нет большой разницы в общем времени счёта.

Чтобы сократить разницу ещё сильнее, давайте немного улучшим наш код.

Мы перенесём планирование очередного вызова в начало count() :

let i = 0; let start = Date.now(); function count() < // перенесём планирование очередного вызова в начало if (i < 1e9 - 1e6) < setTimeout(count); // запланировать новый вызов >do < i++; >while (i % 1e6 != 0); if (i == 1e9) < alert("Done in " + (Date.now() - start) + 'ms'); >> count();

Теперь, когда мы начинаем выполнять count() и видим, что потребуется выполнить count() ещё раз, мы планируем этот вызов немедленно, перед выполнением работы.

Если вы запустите этот код, то легко заметите, что он требует значительно меньше времени.

Всё просто: как вы помните, в браузере есть минимальная задержка в 4 миллисекунды при множестве вложенных вызовов setTimeout . Даже если мы указываем задержку 0 , на самом деле она будет равна 4 мс (или чуть больше). Поэтому чем раньше мы запланируем выполнение – тем быстрее выполнится код.

Итак, мы разбили ресурсоёмкую задачу на части – теперь она не блокирует пользовательский интерфейс, причём почти без потерь в общем времени выполнения.

Пример 2: индикация прогресса

Ещё одно преимущество разделения на части крупной задачи в браузерных скриптах – это возможность показывать индикатор выполнения.

Обычно браузер отрисовывает содержимое страницы после того, как заканчивается выполнение текущего кода. Не имеет значения, насколько долго выполняется задача. Изменения в DOM отображаются только после её завершения.

С одной стороны, это хорошо, потому что наша функция может создавать много элементов, добавлять их по одному в документ и изменять их стили – пользователь не увидит «промежуточного», незаконченного состояния. Это важно, верно?

В примере ниже изменения i не будут заметны, пока функция не завершится, поэтому мы увидим только последнее значение i :

 > count(); 

…Но, возможно, мы хотим что-нибудь показать во время выполнения задачи, например, индикатор выполнения.

Если мы разобьём тяжёлую задачу на части, используя setTimeout , то изменения индикатора будут отрисованы в промежутках между частями.

Так будет красивее:

 while (i % 1e3 != 0); if (i < 1e7) < setTimeout(count); >> count(); 

Теперь показывает растущее значение i – это своего рода индикатор выполнения.

Пример 3: делаем что-нибудь после события

В обработчике события мы можем решить отложить некоторые действия, пока событие не «всплывёт» и не будет обработано на всех уровнях. Мы можем добиться этого, обернув код в setTimeout с нулевой задержкой.

В главе Генерация пользовательских событий мы видели пример: наше событие menu-open генерируется через setTimeout , чтобы оно возникло после того, как полностью обработано событие «click».

menu.onclick = function() < // . // создадим наше собственное событие с данными пункта меню, по которому щёлкнули мышью let customEvent = new CustomEvent("menu-open", < bubbles: true >); // сгенерировать наше событие асинхронно setTimeout(() => menu.dispatchEvent(customEvent)); >;

Макрозадачи и Микрозадачи

Помимо макрозадач, описанных в этой части, существуют микрозадачи, упомянутые в главе Микрозадачи.

Микрозадачи приходят только из кода. Обычно они создаются промисами: выполнение обработчика .then/catch/finally становится микрозадачей. Микрозадачи также используются «под капотом» await , т.к. это форма обработки промиса.

Также есть специальная функция queueMicrotask(func) , которая помещает func в очередь микрозадач.

Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач перед тем, как выполнить следующую макрозадачу или отобразить изменения на странице, или сделать что-то ещё.

setTimeout(() => alert("timeout")); Promise.resolve() .then(() => alert("promise")); alert("code");

Какой здесь будет порядок?

  1. code появляется первым, т.к. это обычный синхронный вызов.
  2. promise появляется вторым, потому что .then проходит через очередь микрозадач и выполняется после текущего синхронного кода.
  3. timeout появляется последним, потому что это макрозадача.

Более подробное изображение событийного цикла выглядит так:

Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.

Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.

Если мы хотим запустить функцию асинхронно (после текущего кода), но до отображения изменений и до новых событий, то можем запланировать это через queueMicrotask .

Вот пример с индикатором выполнения, похожий на предыдущий, но в этот раз использована функция queueMicrotask вместо setTimeout . Обратите внимание – отрисовка страницы происходит только в самом конце. Как и в случае обычного синхронного кода.

 while (i % 1e3 != 0); if (i < 1e6) < queueMicrotask(count); >> count(); 

Итого

Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со спецификацией):

  1. Выбрать и исполнить старейшую задачу из очереди макрозадач (например, «script»).
  2. Исполнить все микрозадачи:
    • Пока очередь микрозадач не пуста: — Выбрать из очереди и исполнить старейшую микрозадачу
  3. Отрисовать изменения страницы, если они есть.
  4. Если очередь макрозадач пуста – подождать, пока появится макрозадача.
  5. Перейти к шагу 1.

Чтобы добавить в очередь новую макрозадачу:

  • Используйте setTimeout(f) с нулевой задержкой.

Этот способ можно использовать для разбиения больших вычислительных задач на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этих частей.

Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).

Для добавления в очередь новой микрозадачи:

  • Используйте queueMicrotask(f) .
  • Также обработчики промисов выполняются в рамках очереди микрозадач.

События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.

Поэтому queueMicrotask можно использовать для асинхронного выполнения функции в том же состоянии окружения.

Web Workers

Для длительных тяжёлых вычислений, которые не должны блокировать событийный цикл, мы можем использовать Web Workers.

Это способ исполнить код в другом, параллельном потоке.

Web Workers могут обмениваться сообщениями с основным процессом, но они имеют свои переменные и свой событийный цикл.

Web Workers не имеют доступа к DOM, поэтому основное их применение – вычисления. Они позволяют задействовать несколько ядер процессора одновременно.

event loop js что это

Если что Event Loop не находится на уровне движка, и тем более не является его частью. Event Loop обеспечивается исключительно средой выполнения, это либо libuv API в случае Node.js, либо внутренний цикл событий Chrome.

07 апреля 2023

С помощью механизма Event Loop (Цикл событий) становится возможным выполнять асинхронный код в JavaScript.

Event Loop — это специальный механизм на уровне движка js, который координирует работу трёх сущностей: Call Stack (стэк вызовов), Web API (API, предоставляемый браузером), Callback Queue (очередь колбэков).

Работают они следующим образом: движок js анализирует код. Когда он встречает вызов какой-то функции, он перемещает эту функцию в Call Stack. Если эта функция синхронная (например, console.log() ), то она сразу же исполняется, покидает стэк и на её место приходит следующая функция. Если же эта функция асинхронная, например, setTimeout() , обработчик событий, сетевой запрос и т.д., то на помощь приходит браузер со своим Web API (мы же помним, что JavaScript — это однопоточный язык, и сам работать в многопоточном режиме он не может). Event Loop перемещает колбэк асинхронной функции в Web API, а сама асинхронная функция уходит из стэка вызовов. То есть, пока колбэк асинхронной функции находится под управлением Web API, движок js продолжает выполнять другие операции!

Что же происходит с колбэком? В случае, например, setTimeout() , Web API ожидает истечения указанного времени, затем Event Loop перемещает этот колбэк в Callback Queue (очередь колбэков). Когда стэк вызовов освобождается, Event Loop перемещает в него наш колбэк из очереди колбэков, после чего колбэк наконец исполняется и покидает стэк вызовов.

Этот процесс повторяется до тех пор, пока весь js код не будет выполнен.

Здесь представлен наглядный пример работы Event Loop, очень советую ознакомиться!

Параллельная модель и цикл событий.

Параллелизм/Многопоточность в JavaScript работает за счёт цикла событий (event loop), который отвечает за выполнение кода, сбора и обработки событий и выполнения под-задач из очереди (queued sub-tasks). Эта модель весьма отличается от других языков программирования, таких как C и Java.

Концепция жизненного цикла

В следующей секции объясняется теоретическая модель. Современные JavaScript движки внедряют/имплементируют и существенно оптимизируют этот процесс.

Визуальное представление

Для лучшего визуального представления работы Event loop, Вы можете ознакомиться с данным видео: https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=389s

Стек

Вызов любой функции создаёт контекст выполнения (Execution Context (en-US) ). При вызове вложенной функции создаётся новый контекст, а старый сохраняется в специальной структуре данных — стеке вызовов (Call Stack).

function f(b)  var a = 12; return a + b + 35; > function g(x)  var m = 4; return f(m * x); > g(21); 

Когда вызывается функция g , создаётся первый контекст выполнения, содержащий аргументы функции g и локальные переменные. Когда g вызывает f , создаётся второй контекст с аргументами f и её локальными переменными. И этот контекст выполнения f помещается в стек вызовов выше первого. Когда f возвращает результат, верхний элемент из стека удаляется. Когда g возвращает результат, её контекст также удалится, и стек становится пустым.

Куча

Объекты размещаются в куче. Куча — это просто имя для обозначения большой неструктурированной области памяти.

Очередь

Среда выполнения JavaScript содержит очередь задач. Эта очередь — список задач, подлежащих обработке. Каждая задача ассоциируется с некоторой функцией, которая будет вызвана, чтобы обработать эту задачу.

Когда стек полностью освобождается, самая первая задача извлекается из очереди и обрабатывается. Обработка задачи состоит в вызове ассоциированной с ней функции с параметрами, записанными в этой задаче. Как обычно, вызов функции создаёт новый контекст выполнения и заносится в стек вызовов.

Обработка задачи заканчивается, когда стек снова становится пустым. Следующая задача извлекается из очереди и начинается её обработка.

Цикл событий

Модель событийного цикла ( event loop ) называется так потому, что отслеживает новые события в цикле:

while (queue.waitForMessage())  queue.processNextMessage(); > 

queue.waitForMessage ожидает поступления задач, если очередь пуста.

Запуск до завершения

Каждая задача выполняется полностью, прежде чем начнёт обрабатываться следующая. Благодаря этому мы точно знаем: когда выполняется текущая функция – она не может быть приостановлена и будет целиком завершена до начала выполнения другого кода (который может изменить данные, с которыми работает текущая функция). Это отличает JavaScript от такого языка программирования как C. Поскольку в С функция, запущенная в отдельном потоке, в любой момент может быть остановлена, чтобы выполнить какой-то другой код в другом потоке.

У данного подхода есть и минусы. Если задача занимает слишком много времени, то веб-приложение не может обрабатывать действия пользователя в это время (например, скролл или клик). Браузер старается смягчить проблему и выводит сообщение «скрипт выполняется слишком долго» («a script is taking too long to run») и предлагает остановить его. Хорошей практикой является создание задач, которые исполняются быстро, и если возможно, разбиение одной задачи на несколько мелких.

Добавление событий в очередь

В браузерах события добавляются в очередь в любое время, если событие произошло, а так же если у него есть обработчик. В случае, если обработчика нет – событие потеряно. Так, клик по элементу, имеющему обработчик события по событию click , добавит событие в очередь, а если обработчика нет – то и событие в очередь не попадёт.

Вызов setTimeout добавит событие в очередь по прошествии времени, указанного во втором аргументе вызова. Если очередь событий на тот момент будет пуста, то событие обработается сразу же, в противном случае событию функции setTimeout придётся ожидать завершения обработки остальных событий в очереди. Именно поэтому второй аргумент setTimeout корректно считать не временем, через которое выполнится функция из первого аргумента, а минимальное время, через которое она сможет выполниться.

Нулевые задержки

Нулевая задержка не даёт гарантии, что обработчик выполнится через ноль миллисекунд. Вызов setTimeout с аргументом 0 (ноль) не завершится за указанное время. Выполнение зависит от количества ожидающих задач в очереди. Например, сообщение »this is just a message» из примера ниже будет выведено на консоль раньше, чем произойдёт выполнение обработчика cb1. Это произойдёт, потому что задержка – это минимальное время, которое требуется среде выполнения на обработку запроса.

(function ()  console.log("this is the start"); setTimeout(function cb()  console.log("this is a msg from call back"); >); console.log("this is just a message"); setTimeout(function cb1()  console.log("this is a msg from call back1"); >, 0); console.log("this is the end"); >)(); // "this is the start" // "this is just a message" // "this is the end" // "this is a msg from call back" // "this is a msg from call back1" 

Связь нескольких потоков между собой

Web Worker или кросс-доменный фрейм имеют свой собственный стек, кучу и очередь событий. Два отдельных событийных потока могут связываться друг с другом, только через отправку сообщений с помощью метода postMessage . Этот метод добавляет сообщение в очередь другого, если он конечно принимает их.

Никогда не блокируется

Очень интересное свойство цикла событий в JavaScript, что в отличие от множества других языков, поток выполнения никогда не блокируется. Обработка I/O обычно осуществляется с помощью событий и колбэк-функций, поэтому даже когда приложение ожидает запрос от IndexedDB или ответ от XHR, оно может обрабатывать другие процессы, например пользовательский ввод.

Существуют хорошо известные исключения как alert или синхронный XHR, но считается хорошей практикой избегать их использования.

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 7 авг. 2023 г. by MDN contributors.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *