Что ты такое, 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
Скорее всего, личные задачи у вас будут приоритетнее. Event Loop с этим согласен :) Остается упорядочить задачи от заказчика.
Конечно, первое, что приходит в голову — задать каждому заказчику приоритет, и выстроить их в очередь. Второе — определить, как именно будут обрабатываться задачи от каждого заказчика — по одной, все сразу или, может быть, пачками.
Взглянем на эту схему:
Рисунок 2
На основе этой схемы строится вся работа Event Loop. После того как Call Stack станет пустым (закончились личные задачи), Event Loop первым делом идет к заказчику Tasks и просит у него только одну, первую в очереди задачу, передает ее в CallStack и идет дальше. Следующий заказчик — Microtasks. У него Event Loop берет все задачи сразу и выполняет их. Далее он идет к Render и выполняет задачи от него. Задачи от Render оптимизируются браузером и, если он посчитает, что в этом цикле не нужно ничего перерисовывать, то Event Loop просто пойдет дальше.
После Render цикл снова повторяется и так пока вкладка браузера не закроется и не завершится процесс.
Если у кого-то из заказчиков не оказалось задач, то 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, и что 'творится' в примерах кода выше.
Всем пока :) И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал :)