Событийный цикл: микрозадачи и макрозадачи
Поток выполнения в браузере, равно как и в Node.js, основан на событийном цикле.
Понимание работы событийного цикла важно для оптимизаций, иногда для правильной архитектуры.
В этой главе мы сначала разберём теорию, а затем рассмотрим её практическое применение.
Событийный цикл
Идея событийного цикла очень проста. Есть бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.
Общий алгоритм движка:
- Пока есть задачи:
- выполнить их, начиная с самой старой
- Бездействовать до появления новой задачи, а затем перейти к пункту 1
Это формализация того, что мы наблюдаем, просматривая веб-страницу. Движок JavaScript большую часть времени ничего не делает и работает, только если требуется исполнить скрипт/обработчик или обработать событие.
- Когда загружается внешний скрипт , то задача – это выполнение этого скрипта.
- Когда пользователь двигает мышь, задача – сгенерировать событие mousemove и выполнить его обработчики.
- Когда истечёт таймер, установленный с помощью setTimeout(func, . ) , задача – это выполнение функции func
- И так далее.
Задачи поступают на выполнение – движок выполняет их – затем ожидает новые задачи (во время ожидания практически не нагружая процессор компьютера)
Может так случиться, что задача поступает, когда движок занят чем-то другим, тогда она ставится в очередь.
Очередь, которую формируют такие задачи, называют «очередью макрозадач» (macrotask queue, термин v8).
Например, когда движок занят выполнением скрипта, пользователь может передвинуть мышь, тем самым вызвав появление события mousemove , или может истечь таймер, установленный setTimeout , и т.п. Эти задачи формируют очередь, как показано на иллюстрации выше.
Задачи из очереди исполняются по правилу «первым пришёл – первым ушёл». Когда браузер заканчивает выполнение скрипта, он обрабатывает событие mousemove , затем выполняет обработчик, заданный setTimeout , и так далее.
Пока что всё просто, не правда ли?
Отметим две детали:
- Рендеринг (отрисовка страницы) никогда не происходит во время выполнения задачи движком. Не имеет значения, сколь долго выполняется задача. Изменения в DOM отрисовываются только после того, как задача выполнена.
- Если задача выполняется очень долго, то браузер не может выполнять другие задачи, обрабатывать пользовательские события, поэтому спустя некоторое время браузер предлагает «убить» долго выполняющуюся задачу. Такое возможно, когда в скрипте много сложных вычислений или ошибка, ведущая к бесконечному циклу.
Это была теория. Теперь давайте взглянем, как можно применить эти знания.
Пример 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 делает часть работы (*) , а затем, если необходимо, планирует свой очередной запуск (**) :
- Первое выполнение производит счёт: i=1…1000000.
- Второе выполнение производит счёт: i=1000001…2000000.
- …и так далее.
Теперь если новая сторонняя задача (например, событие 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");
Какой здесь будет порядок?
- code появляется первым, т.к. это обычный синхронный вызов.
- promise появляется вторым, потому что .then проходит через очередь микрозадач и выполняется после текущего синхронного кода.
- timeout появляется последним, потому что это макрозадача.
Более подробное изображение событийного цикла выглядит так:
Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.
Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.
Если мы хотим запустить функцию асинхронно (после текущего кода), но до отображения изменений и до новых событий, то можем запланировать это через queueMicrotask .
Вот пример с индикатором выполнения, похожий на предыдущий, но в этот раз использована функция queueMicrotask вместо setTimeout . Обратите внимание – отрисовка страницы происходит только в самом конце. Как и в случае обычного синхронного кода.
while (i % 1e3 != 0); if (i < 1e6) < queueMicrotask(count); >> count();
Итого
Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со спецификацией):
- Выбрать и исполнить старейшую задачу из очереди макрозадач (например, «script»).
- Исполнить все микрозадачи:
- Пока очередь микрозадач не пуста: — Выбрать из очереди и исполнить старейшую микрозадачу
- Отрисовать изменения страницы, если они есть.
- Если очередь макрозадач пуста – подождать, пока появится макрозадача.
- Перейти к шагу 1.
Чтобы добавить в очередь новую макрозадачу:
- Используйте setTimeout(f) с нулевой задержкой.
Этот способ можно использовать для разбиения больших вычислительных задач на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этих частей.
Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).
Для добавления в очередь новой микрозадачи:
- Используйте queueMicrotask(f) .
- Также обработчики промисов выполняются в рамках очереди микрозадач.
События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.
Поэтому queueMicrotask можно использовать для асинхронного выполнения функции в том же состоянии окружения.
Web Workers
Для длительных тяжёлых вычислений, которые не должны блокировать событийный цикл, мы можем использовать Web Workers.
Это способ исполнить код в другом, параллельном потоке.
Web Workers могут обмениваться сообщениями с основным процессом, но они имеют свои переменные и свой событийный цикл.
Web Workers не имеют доступа к DOM, поэтому основное их применение – вычисления. Они позволяют задействовать несколько ядер процессора одновременно.
Что ты такое, Event Loop? Или как устроен цикл событий в браузере Chrome
Как думаете, что произойдет, если запустить в консоли браузера этот фрагмент кода?
function foo() < setTimeout(foo, 0); >foo();
function foo() < Promise.resolve().then(foo); >foo();
Если вы также, как и я, прочитали кучу статей про Event Loop, Main Thread, таски, микротаски и прочее, но затрудняетесь ответить на вопросы выше — эта статья для вас.
Итак, приступим. Код каждой HTML-страницы в браузере выполняется в Main Thread. Main Thread — это основной поток, где браузер выполняет JS, делает перерисовки, обрабатывает пользовательские действия и многое другое. По сути, это то место, где движок JS интегрирован в браузер.
Проще всего разобраться, глядя на схему:
Рисунок 1
Мы видим, что единственное место, через которое задачи могут попасть в Call Stack и выполниться — это Event Loop. Представьте, что вы оказались на его месте. И ваша работа успевать ‘разгребать’ задачи. Задачи могут быть двух типов:
- Личные — выполнение основного JavaScript-кода на сайте (далее будем считать, что он уже выполнился)
- Задачи от заказчиков — Render, Microtasks и Tasks
Конечно, первое, что приходит в голову — задать каждому заказчику приоритет, и выстроить их в очередь. Второе — определить, как именно будут обрабатываться задачи от каждого заказчика — по одной, все сразу или, может быть, пачками.
Взглянем на эту схему:

Рисунок 2
На основе этой схемы строится вся работа Event Loop.
После того как мы начали выполнять какой-либо script, в очередь Tasks ставится задача с выполнением этого скрипта. По мере выполнения этого кода, нам встречаются задачи от разных заказчиков, которые ставятся в соответствующие очереди. После того как завершается задача по выполнению скрипта (задача от Tasks), Event Loop идет к Microtasks (после задачи от Tasks Event Loop берет задачи от Microtasks). У него Event Loop берет задачи до тех пор, пока они не закончатся. Это значит, что если время их добавления равно времени их выполнения, то Event Loop будет бесконечно их разгребать.
Далее он идет к Render и выполняет задачи от него. Задачи от Render оптимизируются браузером и, если он посчитает, что в этом цикле не нужно ничего перерисовывать, то Event Loop просто пойдет дальше. Далее Event Loop снова берет задачи от Tasks и просит у него только одну, первую в очереди задачу, передает ее в CallStack и идет дальше по циклу.
Если у кого-то из заказчиков не оказалось задач, то Event Loop просто идет к следующему. И, наоборот, если у заказчика задачи занимают много времени, то остальные заказчики будут ждать своей очереди. А если задачи от какого-то заказчика оказались бесконечными, то Call Stack переполняется, и браузер начинает ругаться:

Рисунок 3
Теперь, когда мы поняли как работает Event Loop, пришло время разобраться, что будет после выполнения фрагментов кода в начале этой статьи.
function foo() < setTimeout(foo, 0); >foo();
Мы видим, что функция foo вызывает сама себя рекурсивно через setTimeout внутри, но при каждом вызове она создает задачу заказчика Tasks. Как мы помним, в цикле Event Loop при выполнении очереди задач от Tasks берет только 1 задачу в цикл. И далее происходит выполнение задач от Microtasks и Render. Поэтому этот фрагмент кода не заставит Event Loop страдать и вечно разгребать его задачи. Но будет подкидывать новую задачу для заказчика Tasks на каждом круге.
Давайте попробуем выполнить этот скрипт в браузере Google Chrome. Для этого я создал простой HTML-документ и подключил в нем script.js с этим фрагментом кода. После открытия документа заходим в инструменты разработчика, и открываем вкладку Perfomance и жмем там кнопку ‘start profiling and reload page’:

Рисунок 4
Видим, что задачи от Tasks выполняются по одной в цикл, примерно раз в 4ms.
Рассмотрим вторую задачку:
function foo() < Promise.resolve().then(foo); >foo();
Здесь мы видим тоже самое, что и в примере выше, но вызов foo добавляет задачи от Microtasks, а они выполняются все, пока не закончатся. А это значит, что пока Event Loop не закончит их, перейти к следующему заказчику он не сможет 🙁 И мы видим снова грустную картинку.
Взглянем на это в интрументах разработчкика:

Рисунок 5
Мы видим, что микротаски выполняются примерно раз в 0.1ms, и это в 40 раз быстрее, чем очередь Tasks. Все потому, что они выполняются все и сразу. В нашем примере очередь движется бесконечно. Для визуализации я уменьшил ее до 100 000 итераций.
Надеюсь, эта статья была вам полезной, и теперь вы понимаете, как работает Event Loop, и что ‘творится’ в примерах кода выше.
Всем пока 🙂 И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал 🙂
- event loop
- javascript
- requestanimationframe
- web api
- цикл событий
Что такое event loop ?
В первую очередь следует запомнить, что EventLoop не является частью JavaScript. EventLoop — это механизм, позволяющий использовать неблокирующую модель ввода-вывода. Т.е. он позволяет выполнять операции без задержки, т.к. не дожидается выполнения каждой из них. Он предоставляется средой — браузером, NodeJS и т.п.
Наш код обрабатывается движком JavaScript.
Как движок JavaScript обрабатывает код?
В начале работы он создает execution context (контекст выполнения) и сохраняет там информацию об объявленных переменных. Execution context — это окружение, в котором происходит выполнения кода. Это контекст помещается в CallStack.
CallStack — является частью движка JavaScript. Код построчно считывается, помещается в CallStack и выполняется. При этом, если движок находит функцию, он создает новый контекст выполнения для этой функции и помещает его в стек.
Код обрабатывается по принципу “первый пришел — первый ушел”. При этом, если вызовется func1, которая вызовет внутри себя func2, func2 первая закончит свое выполнение и уйдет из стека, только после этого закончит свое выполнение и уйдет из стека func1:
Пока функция не закончит свое выполнения, другие функции выполнятся не будут. Если в CallStack попадет функция, в которой будут происходить какие-то долгие вычисления, то будет заблокировано все остальное. Не будет происходить даже отрисовка.
Тут на помощь приходит EventLoop, потому что он помогает избегать блокировок и зависаний в программе, т.к. выполняет операции не дожидаясь их завершения.
EventLoop предоставляет несколько очередей. Сейчас нас будут интересовать 2 из них:
- очередь макрозадач
- очередь микрозадач
Допустим, у нас есть такой код:
Обработчик поместит func1 в стек, в процессе выполнения он встретит setTimeout.
- движок отправляет setTimeout в WebApi. WebApi предоставляет timeout, обработку слушателей событий, события загрузки изображений, файлов, отправку fetch запросов.
- после того, как таймер закончился, callback setTimeout перемещается в очередь макрозадач
- оттуда он перемещается в CallStack
Посмотрим еще один пример:
Эти слушатели событий будут зарегистрированы пока мы их явно не удалим через removeEventListener
Очередь макрозадач состоит из:
- таймеры (setTimeout)
- события (клик, загрузка изображения, ввод чего-то в инпут и т.д)
- Браузерные нюансы (рендер, input/output и тд)
Но что с очередью микрозадач?
Во-первых, нужно запомнить, что микрозадачи выполняются в приоритете. Сначала выполняются все микрозадачи, только после этого выполняется одна макрозадача.
Что попадает в очередь микрозадач?
- promise
- queueMicrotask (функция для того, чтобы явно создать микрозадачу)
- mutationObserver (специальный инструмент, который позволяет следить за DOM nodes)
Что будет происходить при выполнении этой функции?
- Движок встретит первыКой setTimeout, отправит его на регистрацию WebApi, по истечении таймера WebApi перемещает его callback в очередь макрозадач
- Аналогичные действия происходят со вторым setTimeout
- Дальше движок встретит Promise и отправит его callback в очередь микрозадач
- Далее выполняется синхронный код console.log(2)
- Когда стек освобождается, в него попадают задачи из очереди микрозадач. В данном примере — callback из первого промиса
- Движок начинает исполнять этот callback, снова встречает в нем Promise и добавляет его callback в очередь микрозадач
- После освобождения стека у нас остается одна задача в очереди микрозадач, движок ее выполняет
- Только после этого будут выполнены задачи из очереди макрозадач
Кроме того EventLoop представляет очередь рендеринга. Задачи из этой очереди выполняются с частотой обновления экрана (обычно 60 FPS). Обычно они выполняются после выполнения микрозадач, но перед выполнением макрозадач.
Асинхронность Event loop

В JavaScript асинхронность — основной инструмент, который обрабатывает запросы параллельно с загрузкой веб-страницы. Сейчас невозможно представить интернет, где все запросы на сервер отправлялись бы с перезагрузкой страницы.
Любые данные от сервера запрашиваются асинхронно: отправляется запрос (XMLHttpRequest или XHR), и код не ждёт его возвращения, продолжая выполняться. Когда же сервер отвечает, объект XHR получает уведомление об этом и запускает функцию⚙️ обратного вызова — callback , который передали в него перед отправкой запроса.
Если правильно использовать инструменты языка , то выполнение запроса, который происходит последовательно и в одном потоке, никак не мешает приёму событий и реакции на них — человек спокойно работает с интерфейсом, не замечая лагов, сбоев и зависаний.
Видео
Event loop
Event loop в JavaScript — менеджер асинхронных вызовов.
Чтобы этот хитрый процесс слаженно работал, в JavaScript реализован механизм для управления очерёдностью исполнения кода . Поскольку это однопоточный язык , возникла необходимость «вклиниваться» в текущий контекст исполнения. Этот механизм называется event loop — событийный цикл.
С английского loop переводится как «петля», что отлично отражает смысл: мы имеем дело с закольцованной очередью.
Event loop регулирует последовательность исполнения контекстов — стек. Он формируется, когда сработало событие или была вызвана функция⚙️. Реакция на событие помещается в очередь исполнения, в event loop , который последовательно, с каждым циклом выполняет попадающий в него код . При этом привязанная к событию функция⚙️ вызывается следующей после текущего контекста исполнения.
В JavaScript постоянно работают связанные между собой синхронная и асинхронная очереди выполнения. Синхронная — stack — формирует очередь и пробрасывает в асинхронную — event loop — вызовы функций⚙️, которые будут выполнены после текущего запланированного исполняемого контекста.
Чтобы данные находились в консистентном состоянии, каждая функция⚙️ должна быть выполнена до конца. Это обусловлено однопоточностью JavaScript и некоторыми другими особенностями, например характерными для функциональных ⚙️языков программирования замыканиями. Поэтому единственный поток представлен в виде очереди контекстов исполнения, в которой и происходит «вклинивание» функций⚙️, прошедших через цикл событий.
Описание
JavaScript это однопоточный язык: одновременно может выполняться только одна задача. Обычно в этом нет ничего сложного, но теперь представьте, что вы запускаете задачу, которая занимает 30 секунд. Да. Во время этой задачи мы ждем 30 секунд, прежде чем что-либо еще может произойти (по умолчанию JavaScript запускается в главном потоке браузера, поэтому весь пользовательский интерфейс будет ждать) Сейчас 2021 год, никто не хочет медленный сайт который тупит.
К счастью, браузер предоставляет нам некоторые функции, которые сам механизм JavaScript не предоставляет: Web API. Который включает в себя DOM API, setTimeout, HTTP-запросы и так далее. Это может помочь нам создать асинхронное неблокирующее поведение .
Когда мы вызываем функцию, она добавляется в call stack(стек вызовов). Стек вызовов является частью механизма JS, это не зависит от браузера. Это классический взгляд на стек, т.е first in , last out . Когда функция возвращает значение, она «выталкивается» из стека.
function great() return 'Hello' > function respond() return setTimeout(() => alert('Hey!'), 1000) > great() respond()
Функция respond возвращает функцию setTimeout . SetTimeout предоставляется нам через Web-API : он позволяет нам делить задачи, не блокируя основной поток. Callback функция, которую мы передали в функцию setTimeout , лямбда функция () => добавляется в Web-API . Тем временем setTimeout и responde извлекаются из стека и возвращают свои значения.
В Web-API таймер работает до тех пор, пока второй аргумент, который мы передали ему, не подождет 1000 мс. Callback не сразу добавляется в стек вызовов, а передается в нечто, называемое очередью.
Это может сбивать с толку: это не означает, что callback функция добавляется в стек вызовов (таким образом, возвращает значение) через 1000 мс! Он просто добавляется в очередь через 1000 мс. Но в этой очереди, функция должна ждать пока придет ее черёд.
Теперь это та часть, которую мы все ждали. Время для event loop выполнить единственную задачу: соединить очередь со стеком вызовов! Если стек вызовов пуст, то есть, если все ранее вызванные функции вернули свои значения и были извлечены из стека, первый элемент в очереди добавляется в стек вызовов. В этом случае никакие другие функции не были вызваны, что означает, что стек вызовов был пуст к тому времени, когда callback функция была первым элементом в очереди.
callback добавляется в стек вызовов, вызывается и возвращает значение, а также извлекается из стека.
Смотреть весело, но вы не сможете полностью понять тему, не работая с ней снова и снова. Попробуйте выяснить, что появится в консоли, если мы запустим следующее:
const foo = () => console.log('First') const bar = () => setTimeout(() => console.log('Second'), 500) const baz = () => console.log('Third') bar() foo() baz()
Давайте посмотрим, что происходит, когда мы запускаем этот код в браузере:
Мы вызываем bar , которая возвращает функцию setTimeout . Callback который мы передали в setTimeout добавляется в Web API , функция setTimeout и bar извлекаются из стека вызовов.
Таймер запускается, тем временем foo вызывается и записывает в журнал First . foo возвращает undefined , baz вызывается и callback добавляется в очередь baz логирует Third . Цикл обработки событий видит, что коллстек пуст после возврата baz , после чего колбэк добавляется в стек вызовов. Callback логирует Second .
Надеюсь, что это заставит вас чувствовать себя более уверено с циклом событий event loop !
Не беспокойтесь, если это все еще кажется запутанным, самое важное — понять, откуда могут возникнуть определенные ошибки или специфическое поведение.

Проблемы?
Пишите в Discord или телеграмм чат, а также подписывайтесь на наши новости
Вопросы:
- Инструмент, который выводит контекст исполнения функции из синхронного потока
- Инструмент, который исполняет код построчно
- Инструмент, который обрабатывает запросы параллельно с загрузкой веб-страниц
Менеджер асинхронных вызовов:
- stack
- Event loop
- Объекты высшего класса
Инструмент, выполняющий код с задержкой в миллисекундах:
Для того чтобы понять, на сколько вы усвоили этот урок, пройдите тест в мобильном приложении нашей школы по этой теме или в нашем телеграм боте.

Ссылки:
- Объяснение работы EventLoop в JavaScript
- Как управлять event loop в JavaScript
- Справочник javascript
- Статья: Объяснение Event Loop в Javascript с помощью визуализации
- Статья: JavaScript Visualized: Promises & Async/Await
Contributors ✨
Thanks goes to these wonderful people (emoji key):
