Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

  1. Главная
  2. Блог
  3. Разработка программного обеспечения

Разработка программного обеспечения

Объяснение цикла событий JavaScript: Стек вызовов, микрозадачи и таймеры с примерами

Почему setTimeout, равный нулю, по-прежнему работает последним? Как один поток может обрабатывать так много одновременно? Узнайте, как на самом деле работает цикл событий, протестируйте его с помощью кода, который мы запустили, и решите классический вопрос на собеседовании.

9 мин чтения
Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Главное

  • JavaScript выполняет ваш код в одном потоке, по частям.
  • Когда стек вызовов пуст, сначала запускаются все микрозадачи, а затем одна задача.
  • setTimeout(fn, 0) означает «как можно скорее после минимальной задержки», а не «сейчас».
  • Длительная синхронная работа все блокирует, поэтому разбейте или переместите в воркер.

Запустите несколько строк с setTimeout и обещанием, и порядок вывода может вас удивить. Цикл событий JavaScript объясняет, почему. Это механизм, который позволяет однопоточному языку обрабатывать таймеры, сетевые вызовы и щелчки без зависаний.

Как только вы это поймете, целый класс запутанных ошибок начнет обретать смысл. Мы запускали каждый пример на Node.js 24, и в результате этого запуска были получены выходные данные. Основные правила упорядочивания также применяются в браузерах, и мы указываем, в чем отличия Node.js. Вы получите больше от этого, если уже знаете основы обещаний JavaScript.

Почему существует цикл событий

JavaScript выполняет ваш код в одном основном потоке, поэтому одновременно выполняется только одна его часть. Если бы он просто ждал сетевого запроса, страница зависала бы до тех пор, пока не придет ответ.

Так что JavaScript не ждет. Медленная работа передается хост-среде, то есть браузеру или Node.js, который может обрабатывать ее в фоновом режиме. Когда эта работа завершается, обратный вызов помещается в очередь. Задача цикла событий — переместить обратные вызовы из очереди в стек, когда он свободен.

Представьте себе шеф-повара, работающего в одиночку. Шеф-повар ставит блюдо в духовку и устанавливает таймер, а не смотрит на него. Когда прозвенит таймер, задание попадает в список, и шеф-повар берет его в руки после того, как выполнит то, что сейчас находится в руках.

Движущиеся части

Часть Что он делает Примеры
Стек вызовов Отслеживает функции, выполняемые в данный момент Ваш синхронный код
API-интерфейсы хоста Работайте вне вашего кода Таймеры, fetch, чтение файлов
Очередь задач Содержит обратные вызовы, готовые к запуску setTimeout, щелчки, ввод/вывод
Очередь микрозадач Содержит обратные вызовы с более высоким приоритетом .then, await, queueMicrotask
Цикл событий Решает, что будет дальше Работает непрерывно

Задачи иногда называют макрозадачами. Строго говоря, источников задач может быть несколько, и точные детали различаются в зависимости от среды.

Правила

  1. Запускайте весь синхронный код, пока стек вызовов не станет пустым.
  2. Запустите все микрозадачи, включая любые новые, добавленные по пути.
  3. В браузере отобразите обновление, если оно необходимо.
  4. Возьмите одну задачу из очереди и запустите ее.
  5. Вернитесь к шагу 2.

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

Посмотреть заказ

console.log("1 sync");
setTimeout(() => console.log("5 timeout"), 0);
Promise.resolve().then(() => console.log("3 promise"));
queueMicrotask(() => console.log("4 microtask"));
console.log("2 sync");

// 1 sync
// 2 sync
// 3 promise
// 4 microtask
// 5 timeout

Вот что произошло. Две строки console.log синхронны, поэтому они выполняются первыми. Обратный вызов промиса и queueMicrotask попали в очередь микрозадач и запустились, как только стек опустел, в том порядке, в котором они были добавлены. Обратный вызов таймера должен был дождаться хода задачи, хотя его задержка была равна нулю.

Микрозадачи выполняются между таймерами

После каждой отдельной задачи очередь микрозадач снова очищается. Этот пример доказывает это:

setTimeout(() => {
  console.log("timer 1");
  Promise.resolve().then(() => console.log("promise inside timer 1"));
}, 0);
setTimeout(() => console.log("timer 2"), 0);

// timer 1
// promise inside timer 1
// timer 2

Промис, созданный во время первого таймера, выполнялся до второго таймера. Современные браузеры и Node.js 11 и более поздних версий ведут себя таким образом.

Где ожидание подходит

await разбивает функцию на две части. Все, что происходит до него, выполняется синхронно, а все, что происходит после него, запланировано как микрозадача. Многих это удивляет:

async function task() {
  console.log("A before await");
  await null;
  console.log("C after await");
}

task();
console.log("B after calling task");

// A before await
// B after calling task
// C after await

Функция async не запускается в фоновом режиме. Он выполняется в том же потоке, пока не достигнет своего первого await. Это применимо даже в случае ожидания чего-то, что не является настоящим обещанием, например null. Дополнительную информацию об этом синтаксисе можно найти в нашем руководстве по асинхронному/ожиданию JavaScript.

Блокировка петли

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

const start = Date.now();
setTimeout(() => console.log("timer ran after", Date.now() - start, "ms"), 0);
while (Date.now() - start < 500) {}

// timer ran after about 500 ms

Задержка таймера — это минимум, а не гарантия. Он запустился только тогда, когда стек освободился. На веб-странице такой длинный цикл означает, что клики не реагируют и анимация останавливается. На сервере каждый второй запрос должен ждать, поэтому один медленный синхронный вызов может нанести вред всем пользователям процесса Node.js.

Общие причины блокировки включают огромные циклы, анализ очень больших JSON, синхронные файловые функции, такие как readFileSync ​​в обработчиках запросов, и катастрофические регулярные выражения.

Совет. В Chrome DevTools на панели «Производительность» задачи длительностью более 50 мс помечаются как длинные. Это быстрый способ найти причину, по которой страница работает медленно.

Оставайтесь отзывчивыми

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

const items = [0, 1, 2, 3, 4];

async function processAll() {
  for (const item of items) {
    if (item === 2) await new Promise((r) => setTimeout(r, 0));
    console.log("item", item);
  }
}

setTimeout(() => console.log("other work gets a turn"), 0);
processAll();

// item 0
// item 1
// other work gets a turn
// item 2
// item 3
// item 4

Другой таймер сработал посередине, потому что мы выполнили настоящую задачу. Реальный код будет выдавать каждые несколько элементов или миллисекунд. Для действительно ресурсоемкой работы, например обработки изображений, используйте веб-воркер в браузерах или рабочий поток в Node.js, который выполняется в отдельных потоках.

голодание микрозадач

Поскольку цикл полностью очищает очередь микрозадач перед тем, как двигаться дальше, микрозадача, которая продолжает планировать больше, может остановить все остальное. Мы протестировали это на 100 000 связанных микрозадачах:

let count = 0;
let timerFired = false;
setTimeout(() => { timerFired = true; }, 0);

function spin() {
  count++;
  if (count < 100000) queueMicrotask(spin);
  else console.log("timer fired during microtasks?", timerFired);
}
queueMicrotask(spin);

// timer fired during microtasks? false

Таймер не мог работать, пока все они не будут выполнены. Если бы эта цепочка никогда не заканчивалась, таймеры никогда бы не сработали, а в браузере страница никогда не перерисовывалась бы. Обратите внимание, что использование await для решенного обещания здесь не поможет, поскольку оно просто добавляет еще одну микрозадачу. Вам нужна реальная задача, например таймер, чтобы дать возможность другим.

Рендеринг в браузерах

Браузер обычно получает возможность рисовать между задачами. Это объясняет распространенную ошибку:

spinner.hidden = false;
doHeavyWork();              // blocks the thread
spinner.hidden = true;

Счетчик никогда не появляется, потому что у браузера не было возможности визуализировать между этими строками. Чтобы исправить это, пропустите раму до того, как начнется тяжелая работа, или передайте эту работу рабочему. Код, изменяющий визуальные эффекты, requestAnimationFrame запускается непосредственно перед следующей отрисовкой, что делает его подходящим инструментом для анимации.

Отличия Node.js

Node.js имеет собственный цикл событий с несколькими этапами, включая таймеры, опрос ввода-вывода и setImmediate. У него также есть process.nextTick, который выполняется в отдельной очереди. Вот что мы увидели при запуске их изнутри асинхронной функции:

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));

// promise
// nextTick
// immediate
// timeout

Во многих статьях утверждается, что nextTick всегда лучше обещаний, а таймер лучше setImmediate. Наш результат показывает, что все не так просто. Наш код уже выполнялся в рамках микрозадачи, поэтому очередь обещаний выполнялась первой. А порядок setTimeout и setImmediate, как известно, варьируется в зависимости от того, откуда они вызываются. Поэтому не пишите код, который зависит от их точного порядка. Если одно событие должно происходить за другим, четко свяжите их.

Интервью-викторина

Прежде чем прочитать ответ, постарайтесь определить порядок:

console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
  console.log("c");
  setTimeout(() => console.log("d"), 0);
  Promise.resolve().then(() => console.log("e"));
});
console.log("f");

Наш прогон напечатал a, f, c, e, b, d. Вот почему:

  1. a и f синхронны, поэтому они выполняются первыми.
  2. Стек пуст, поэтому выполняются микрозадачи: c, затем новая микрозадача e в том же раунде.
  3. Теперь одна задача: таймер b, который был поставлен в очередь первым.
  4. Затем следующая задача: таймер d, который был поставлен в очередь во время выполнения микрозадачи.

Распространенные ошибки

  • Мышление setTimeout является точным. Задержка минимальна.
  • Мышление async означает другой поток. Асинхронный код по-прежнему выполняется в основном потоке между ожиданиями.
  • Использование спящих режимов для исправления порядка. Ожидание фактического состояния гораздо надежнее, что также относится к ненадежным тестам.
  • Использование forEach с await. Между элементами нет ожидания. См. Методы массивов JavaScript.
  • Выполнение тяжелой синхронной работы в обработчике сервера. Один запрос может задержать любой другой запрос.

Итоги

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

Часто задаваемые вопросы

Что такое цикл событий в JavaScript?

Это механизм, который запускает обратные вызовы в очереди, когда стек вызовов пуст. Он позволяет однопоточному JavaScript обрабатывать таймеры, входные данные и ответы сети без блокировки.

Чем микрозадача отличается от задачи?

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

Почему setTimeout с 0 не запускается сразу?

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

Является ли JavaScript однопоточным?

Ваш основной код JavaScript выполняется в одном потоке. Браузеры и Node.js используют другие потоки для некоторой фоновой работы, а воркеры позволяют запускать код параллельно.

Блокирует ли await цикл событий?

Нет. await приостанавливает только эту асинхронную функцию, а в это время может выполняться другой код. Однако длинный синхронный код до или после него все равно может заблокировать цикл.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Тестирование ПО

Пользовательское приемочное тестирование (UAT): процесс, контрольный список и реальные примеры

Приемочное тестирование пользователей объяснило: кто его проводит, чем оно отличается от контроля качества, семиэтапный процесс, шаблон тестового сц…

Читать статью →
Базы данных и SQL

Объяснение значений NULL в SQL: IS NULL, COALESCE, NULLIF и ловушки, которых следует избегать

Значения SQL NULL объяснены с помощью запросов, которые мы выполнили: трехзначная логика, IS NULL, COALESCE, NULLIF, как агрегаты, соединения и сорт…

Читать статью →
Базы данных и SQL

Оператор SQL CASE WHEN: синтаксис, реальные примеры и распространенные ошибки

SQL CASE WHEN объяснен с помощью запросов, которые мы выполняли: простые формы и формы для поиска, порядок условий, повороты с помощью SUM, трюки CO…

Читать статью →
Разработка ПО

Что такое JWT? Веб-токены JSON, объясненные на рабочем примере

Что такое JWT? Объяснение JSON Web Tokens: три части: токен, созданный и измененный в коде, который мы запускали, JWT и сеансы, хранилище, токены об…

Читать статью →
Разработка ПО

Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Цикл событий JavaScript объясняется с помощью кода, который мы запускали: стек вызовов, очереди задач и микрозадач, почему setTimeout 0 не является …

Читать статью →
Разработка ПО

Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

Генераторы Python и доходность, объясненные с помощью кода, который мы запускали: как они приостанавливаются и возобновляются, измеряют экономию пам…

Читать статью →
Искусственный интеллект

Что такое вложения? Простое объяснение векторов и косинусного сходства

Что такое вложения? Простое руководство по векторам, косинусному подобию и семантическому поиску с небольшим примером, который мы использовали, общи…

Читать статью →
DevOps

Сине-зеленое и канареечное развертывание: различия, компромиссы и как выбрать

Объяснение сине-зеленого и канареечного развертывания: как каждый из них работает, стоимость, риски и откат, стабильное группирование пользователей,…

Читать статью →
Автоматизация тестирования

Ненадежные тесты: что их вызывает и как их исправить навсегда

Ненадежные тесты объясняются на демонстрациях, которые мы запускали: основные причины, способы подтверждения и диагностики, правильные исправления в…

Читать статью →
Тестирование ПО

Нагрузочное тестирование и стресс-тестирование: различия, метрики и примеры k6

Объяснение нагрузочного и стресс-тестирования: что доказывает каждое из них, тесты на всплески и выдержки, важные метрики, почему средние значения в…

Читать статью →
Тестирование ПО

Тестовый пример и тестовый сценарий: ключевые различия, примеры и когда использовать каждый

Объяснение тестового случая и тестового сценария: четкие определения, параллельная таблица, одна функция сброса пароля, взятая из сценариев в случаи…

Читать статью →
Базы данных и SQL

Оптимизация SQL-запросов: чтение планов выполнения и исправление медленных запросов с указанием времени

Оптимизация SQL-запросов проверена на миллионе строк: читайте планы выполнения, добавляйте правильные индексы, исправляйте неразмещаемые фильтры, ис…

Читать статью →
Базы данных и SQL

SQL UNION против UNION ALL: различия, примеры и проверка производительности

SQL UNION и UNION ALL объяснены с помощью запросов, которые мы выполнили: дубликаты, правила, которым следуют оба, реальная ошибка данных, измеренна…

Читать статью →
Разработка ПО

Учебное пособие по API JavaScript Fetch: GET, POST, обработка ошибок и тайм-ауты

Учебное пособие по API JavaScript Fetch, протестированное на локальном сервере: GET и POST, почему не выдается ошибка 404, многоразовый помощник, ст…

Читать статью →
Разработка ПО

Обработка файлов Python: чтение, запись, CSV, JSON и pathlib с примерами

Обработка файлов Python объясняется с помощью кода, который мы запускали: открытые режимы, чтение больших файлов, CSV, JSON, pathlib, двоичные файлы…

Читать статью →
Разработка ПО

Классы и объекты Python: учебник ООП с реальными примерами

Классы и объекты Python, объясненные с помощью кода, который мы запускали: __init__, методы, атрибуты класса и экземпляра, наследование, свойства, м…

Читать статью →
Искусственный интеллект

Что такое большая языковая модель? Как работают LLM, объяснено без шумихи

Что такое большая языковая модель? LLM объясняется простым языком: токены, встраивания, внимание, обучение, температурные и контекстные окна, крошеч…

Читать статью →
DevOps

Команды Linux для начинающих: основное руководство по терминалу с примерами

Команды Linux для начинающих, сгруппированные по задачам: навигация, файлы, просмотр журналов, grep, каналы, разрешения, процессы и архивы, с практи…

Читать статью →
Автоматизация тестирования

Selenium vs Playwright: ключевые различия, сравнение кода и что выбрать

Сравнение Selenium и Playwright: архитектура, ожидание, поддержка браузера и языка, отладка, одинаковый тест в обоих инструментах, советы по миграци…

Читать статью →
Тестирование ПО

Разделение эквивалентности: метод проектирования тестов, объясненный на рабочих примерах

Объяснение эквивалентного разделения: как найти действительные и недействительные разделы, рабочие примеры для возрастов, паролей и купонов, проведе…

Читать статью →
Тестирование ПО

Серьезность и приоритет в тестировании программного обеспечения: различия, матрица и реальные примеры

Объяснение серьезности и приоритета при тестировании программного обеспечения: четкие определения, четырехсторонняя матрица с примерами, шкалы уровн…

Читать статью →
Базы данных и SQL

Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

Руководство по подзапросам SQL с выполненными нами запросами: скалярные подзапросы, IN, FROM и SELECT, коррелированные подзапросы, EXISTS, ловушка N…

Читать статью →
Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Транзакции SQL и ACID, объясненные с помощью банковского перевода, который мы выполнили: COMMIT, ROLLBACK, точки сохранения, блокировки, уровни изол…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →