Что ты такое, 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 интегрирован в браузер.

Проще всего разобраться, глядя на схему:

l0z9q2s-zdltplomxlim269pu7k.png
Рисунок 1

Мы видим, что единственное место, через которое задачи могут попасть в Call Stack и выполниться — это Event Loop. Представьте, что вы оказались на его месте. И ваша работа успевать 'разгребать' задачи. Задачи могут быть двух типов:

  1. Личные — выполнение основного JavaScript-кода на сайте (далее будем считать, что он уже выполнился)
  2. Задачи от заказчиков — Render, Microtasks и Tasks


Скорее всего, личные задачи у вас будут приоритетнее. Event Loop с этим согласен :) Остается упорядочить задачи от заказчика.

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

Взглянем на эту схему:

zhlqffco6t_lo1sxkql-hoqmlmq.png
Рисунок 2

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

После Render цикл снова повторяется и так пока вкладка браузера не закроется и не завершится процесс.

Если у кого-то из заказчиков не оказалось задач, то Event Loop просто идет к следующему. И, наоборот, если у заказчика задачи занимают много времени, то остальные заказчики будут ждать своей очереди. А если задачи от какого-то заказчика оказались бесконечными, то Call Stack переполняется, и браузер начинает ругаться:

1iuki1fqrhhhbokv8u9aszkyzl8.png
Рисунок 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':

g2t2qmmkttfonylvgs0tqkyfbs0.png
Рисунок 4

Видим, что задачи от Tasks выполняются по одной в цикл, примерно раз в 4ms.

Рассмотрим вторую задачку:

function foo() {
  Promise.resolve().then(foo);
}

foo();


Здесь мы видим тоже самое, что и в примере выше, но вызов foo добавляет задачи от Microtasks, а они выполняются все, пока не закончатся. А это значит, что пока Event Loop не закончит их, перейти к следующему заказчику он не сможет :(И мы видим снова грустную картинку.

Взглянем на это в интрументах разработчкика:

kaghkkleewj7u-2_pzrgbkavn4g.png
Рисунок 5

Мы видим, что микротаски выполняются примерно раз в 0.1ms, и это в 40 раз быстрее, чем очередь Tasks. Все потому, что они выполняются все и сразу. В нашем примере очередь движется бесконечно. Для визуализации я уменьшил ее до 100 000 итераций.

Вот и все!

Надеюсь, эта статья была вам полезной, и теперь вы понимаете, как работает Event Loop, и что 'творится' в примерах кода выше.

Всем пока :) И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал :)

© Habrahabr.ru