Event loop: как браузер успевает всё

асинхронность≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Браузер умудряется крутить анимацию, ждать ответ сервера и слушать клики одновременно — хотя ваш JavaScript выполняется в один поток. Весь фокус в event loop: бесконечном цикле, который раздаёт единственному потоку дела строго по одному.

Разделы темы:·

Движок делает ровно одно дело за раз. Чтобы не потерять, где он сейчас находится, он ведёт стек вызовов — стопку карточек. Вызвали функцию — карточка легла сверху, функция вернулась — карточка снялась. Пока стопка не пуста, поток занят.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Стопка не бесконечная. Если функции складываются быстрее, чем снимаются, место заканчивается — и движок честно об этом сообщает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ни setTimeout, ни fetch, ни подписка на клик не описаны в языке. Это функции браузера. Движок просто передаёт поручение: «отсчитай полсекунды и позови вот эту функцию» — и тут же снимает карточку со стека. Считает уже браузер, своими средствами, вне потока JavaScript.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратите внимание: порядок строк в коде ничего не решает. Решает момент, когда задача попала в очередь.

Очередей на самом деле две. У промисов своя — очередь микрозадач, и она разбирается целиком после каждой задачи, до того как дойдёт черёд до таймеров. Что такое промис и зачем он нужен, объяснит статья «Промисы: основы», а полный порядок двух очередей разбирает «Микрозадачи и макрозадачи».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Практический вывод: .then() выполнится всегда раньше, чем setTimeout(f, 0), даже если стоит в коде позже.

Event loop устроен до обидного просто. Он спрашивает: стек пуст? Если да — берёт первую задачу из очереди и кладёт её на стек. Если нет — ждёт. И так бесконечно, пока открыта вкладка.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

ГдеЧто тамКто хозяин
Стек вызововфункции, выполняющиеся прямо сейчасдвижок JavaScript
Web APIтикающие таймеры, запросы, подпискибраузер
Очередь задачколбэки, готовые к выполнениюevent loop
Очередь микрозадачреакции промисов — разбирается целиком после каждой задачидвижок JavaScript

Правило одного дела: пока функция не вернулась, на странице не происходит ничего, что требует JavaScript и раскладки.

Ноль в setTimeout — это не «выполни немедленно», а «поставь в очередь без дополнительной задержки». Задача всё равно дождётся, пока опустеет стек и разберутся микрозадачи.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Зато у приёма есть законное применение: setTimeout(f, 0) откладывает работу «на потом», отпуская поток и давая браузеру вдохнуть между делами.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
разбор вывода
Почему в выводе true, а не число: миллисекунды на разных машинах разные, а порог — тот же самый. Будь «ноль» и правда нулём, двадцать вложенных таймеров прошли бы заметно быстрее 20 мс. Но четырнадцать из них браузер зажимает до 4 мс, и сумма переваливает за порог. След четвёрки виден, хотя само число не напечатано.

Перерисовка страницы — работа того же самого потока. Браузер вставляет её между задачами, стараясь успевать около 60 кадров в секунду. Пока на стеке крутится ваша функция, вставить кадр некуда: анимация стоит, кнопка не нажимается, текст не выделяется. Клики при этом не пропадают — они копятся в очереди и обрушатся разом, когда поток освободится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Лечится это не ускорением кода, а его нарезкой. Считаем небольшую порцию, отдаём поток браузеру через setTimeout — он успевает нарисовать кадр и обработать клики, — и возвращаемся за следующей порцией.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • JavaScript выполняется в один поток: стек вызовов держит одну цепочку функций, и никто не прерывает её на середине.
  • Таймеры, запросы и подписки на события — это Web API браузера, а не язык; ожидание идёт вне потока и не блокирует его.
  • Готовый колбэк не запускается сразу — он встаёт в очередь задач и ждёт, пока event loop увидит пустой стек.
  • setTimeout(f, 0) означает «поскорее», а не «немедленно»: сначала весь синхронный код, затем микрозадачи промисов, потом уже таймер.
  • Отрисовка идёт в том же потоке, поэтому долгая синхронная работа замораживает интерфейс — тяжёлые вычисления режут на порции и отпускают поток между ними.

1/3Что выведет код: console.log(1); setTimeout(() => console.log(2), 0); console.log(3);

2/3Пользователь кликнул по кнопке, пока на стеке крутится долгий цикл. Что произойдёт?

3/3Что на самом деле означает setTimeout(f, 0)?

Предскажите порядок вывода

Не запуская код, выпишите порядок строк в консоли. Потом проверьте себя в браузере.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Порядок: A, D, C, B.

Сначала до конца отрабатывает синхронный код — это A и D. Как только стек опустел, разбирается очередь микрозадач: C. И лишь затем event loop берёт задачу таймера — B.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Почему переменная пустая

Код должен вывести 42, но выводит null. Объясните почему и почините — не «ускоряя» таймер и не добавляя второй.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

console.log выполняется в тот же миг, а колбэк таймера попадёт на стек минимум через 100 мс — к моменту вывода присваивания ещё не было.

Чинится переносом использования внутрь колбэка: работать с данными нужно там, где они уже появились.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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