Урок по треку JavaScript 7 мин

Event loop за 7 минут

Стек вызовов, микро- и макрозадачи, порядок вывода и рендер.

JavaScript выполняет код в одном потоке: в каждый момент работает только одна функция. Сетевые запросы, таймеры и клики при этом не блокируют страницу, потому что их ожидание берёт на себя окружение (браузер или Node), а результаты возвращаются в виде задач в очередь. Цикл, который разбирает эти очереди, и есть event loop.

Стек вызовов

Стек вызовов (call stack) хранит функции, которые выполняются прямо сейчас. Вызвали функцию: она легла на стек. Вернулась: снялась. Пока стек не пуст, ничего другого выполниться не может: ни таймер, ни обработчик клика, ни отрисовка. Поэтому долгий синхронный цикл замораживает страницу.

Задачи и микрозадачи

Асинхронные колбэки попадают в две разные очереди.

Задачи (macrotasks): setTimeout, setInterval, события ввода (клик, клавиатура), сообщения postMessage и MessageChannel, завершение сетевых операций. Строго говоря, в браузере таких очередей несколько, и браузер может отдавать приоритет вводу пользователя, но порядок внутри одного источника сохраняется.

Микрозадачи (microtasks): колбэки then/catch/finally, продолжение после await, queueMicrotask, MutationObserver.

Один оборот цикла

  1. Взять одну задачу из очереди и выполнить её до конца (до пустого стека).
  2. Выполнить все микрозадачи, включая те, что добавились по ходу.
  3. Если пора, отрисовать кадр: колбэки requestAnimationFrame, пересчёт стилей, layout, paint.
  4. Перейти к следующей задаче.

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

console.log('1');
setTimeout(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1 4 3 2

Опасность микрозадач

Очередь микрозадач опустошается полностью. Если микрозадача постоянно добавляет новую, цикл никогда не дойдёт до следующей задачи и до отрисовки: страница зависнет так же, как от while (true). С setTimeout такой рекурсии не было бы: между задачами браузер успевает отрисовать кадр.

Таймеры

  • setTimeout(fn, 0) не значит «сразу». Это «не раньше чем через 0 мс, отдельной задачей после текущей и всех микрозадач».
  • Задержка минимальная, а не точная: если поток занят, таймер подождёт.
  • При вложенности таймеров глубже 5 уровней браузер поднимает минимальную задержку до 4 мс, а в фоновых вкладках таймеры сильно замедляются.
  • Таймеры с одинаковой задержкой срабатывают в порядке постановки.

requestAnimationFrame

requestAnimationFrame(cb) выполняет колбэк прямо перед следующей отрисовкой, обычно 60 раз в секунду (или с частотой экрана). Для анимаций это лучше таймера: нет лишних кадров и рывков, в скрытой вкладке не вызывается.

Долгие задачи

Задача длиннее 50 мс считается долгой (long task): пользователь чувствует задержку реакции, страдает метрика INP. Решения:

  • разбить работу на куски и отдавать управление между ними (setTimeout, scheduler.yield() в Chromium);
  • вынести вычисления в Web Worker, у которого свой поток и свой event loop;
  • для низкоприоритетной работы requestIdleCallback.

Отличия Node.js

В Node event loop реализован в libuv и разбит на фазы: timers, pending callbacks, poll (ввод-вывод), check (setImmediate), close callbacks. Микрозадачи выполняются после каждого колбэка. Ещё есть process.nextTick: его очередь выполняется даже раньше промисов. Порядок setTimeout(fn, 0) и setImmediate в главном модуле не гарантирован, а внутри колбэка ввода-вывода setImmediate всегда первый. Подробнее в треке Node.js.

Частые ошибки

  • Считать, что setTimeout(fn, 0) выполнится раньше промиса.
  • Думать, что async-функция целиком асинхронна. Код до первого await выполняется синхронно.
  • Ожидать, что await уступит место таймерам. Продолжение после await это микрозадача, она идёт раньше любого таймера.
  • Пытаться «подождать отрисовку» через промис: микрозадачи выполняются до рендера.

Что спросят на собесе

  • Как работает event loop, что такое стек, задачи и микрозадачи.
  • Что выведет код с setTimeout, Promise.then и async/await.
  • Почему бесконечные микрозадачи вешают страницу, а бесконечные таймеры нет.
  • Где в цикле происходит отрисовка и что такое requestAnimationFrame.
  • Чем event loop в Node отличается от браузерного.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (34)