Как браузер определяет, что асинхронный код не выполняется?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как браузер определяет, что асинхронный код не выполняется?
Это интересный вопрос, который касается того, как браузер отслеживает состояние выполнения асинхронного кода и управляет event loop'ом. Ответ связан с концепциями event loop, task queue и microtask queue.
Event Loop и его состояния
Browser engine постоянно работает в цикле:
while (eventLoop.waitForTask()) {
// 1. Выполнить все синхронные скрипты (call stack)
const macrotask = eventLoop.nextMacrotask();
if (macrotask) {
execute(macrotask);
}
// 2. Выполнить ВСЕ микротаски
while (microtaskQueue.hasTasks()) {
execute(microtaskQueue.nextMicrotask());
}
// 3. Отрендерить (если нужно)
if (isRepaintTime()) {
repaint();
}
}
Браузер определяет, что асинхронный код не выполняется, по нескольким критериям:
1. Пустой Call Stack
Основной признак — стек вызовов полностью пуст. Это означает, что весь синхронный код выполнен.
console.log('1. Синхронный код');
setTimeout(() => {
console.log('3. Асинхронный код (макротаска)');
}, 0);
Promise.resolve().then(() => {
console.log('2. Асинхронный код (микротаска)');
});
console.log('4. Ещё синхронный код');
// Вывод:
// 1. Синхронный код
// 4. Ещё синхронный код
// -- Call stack пуст, браузер начинает выполнять очереди --
// 2. Асинхронный код (микротаска)
// 3. Асинхронный код (макротаска)
2. Пустая Task Queue (макротаски)
Макротаски (setTimeout, setInterval, fetch callbacks) хранятся в task queue. Браузер проверяет эту очередь:
// Макротаски, которые браузер отслеживает:
// - setTimeout / setInterval
// - fetch и XMLHttpRequest
// - User interactions (click, scroll, etc.)
// - Rendering tasks
// - MessageChannel
setTimeout(() => console.log('Макротаска 1'), 0);
setTimeout(() => console.log('Макротаска 2'), 0);
// Браузер видит в task queue две задачи
// После выполнения первой — ещё есть вторая
// После выполнения второй — очередь пуста
3. Пустая Microtask Queue
Микротаски (Promise callbacks, queueMicrotask) в отдельной очереди.
Promise.resolve()
.then(() => console.log('Микротаска 1'))
.then(() => console.log('Микротаска 2'));
queueMicrotask(() => console.log('Микротаска 3'));
// Браузер выполняет ВСЕ микротаски перед следующей макротаской
4. Нет ожидающих асинхронных операций
Браузер отслеживает все активные асинхронные операции:
// Активная операция — fetch
const promise = fetch('/api/data');
// Браузер знает, что есть ожидающая операция
// Даже если call stack пуст, браузер НЕ считает код завершённым
promise.then(response => {
console.log('Ответ получен');
});
// После получения ответа микротаска (then) выполняется
Как это работает в коде?
const eventLoop = {
callStack: [],
taskQueue: [],
microtaskQueue: [],
pendingOperations: new Set() // fetch, setTimeout, etc.
};
// Браузер постоянно проверяет:
function isAsyncCodeRunning() {
return (
eventLoop.callStack.length > 0 || // Синхронный код
eventLoop.taskQueue.length > 0 || // Макротаски
eventLoop.microtaskQueue.length > 0 || // Микротаски
eventLoop.pendingOperations.size > 0 // Операции (fetch, setTimeout)
);
}
if (!isAsyncCodeRunning()) {
console.log('Весь асинхронный код выполнен');
// Браузер может:
// - Заснуть (idle)
// - Выполнить другие задачи ОС
// - Слушать пользовательские события
}
Practical example
// Пример 1: Простой setTimeout
console.log('Start');
setTimeout(() => {
console.log('Timeout finished');
}, 100);
console.log('End');
// Call stack: ['console.log', 'setTimeout']
// После обоих console.log -> call stack пуст
// setTimeout добавлен в task queue
// Браузер ждёт 100ms
// После 100ms выполняет callback
// Вывод:
// Start
// End
// Timeout finished (после паузы)
// Пример 2: Promise
console.log('Start');
fetch('/api').then(res => res.json());
console.log('End');
// Call stack: empty после двух console.log
// Браузер видит, что есть fetch в pendingOperations
// .then() добавляется в microtask queue (будет выполнена после fetch)
// Браузер ждёт ответа
// Вывод:
// Start
// End
// (depois ответа сервера)
// { ...json response }
Debug Tools
Модерн браузеры позволяют видеть это в Performance tab:
// Можно использовать Performance API
performance.mark('start');
setTimeout(() => {
performance.mark('end');
performance.measure('async-task', 'start', 'end');
}, 100);
// В DevTools -> Performance tab видно timeline выполнения
Что мешает коду завершиться?
// 1. Бесконечный setInterval
const intervalId = setInterval(() => {
console.log('Будет выполняться вечно');
}, 1000);
// Код НИКОГДА не завершится, пока не вызовешь:
clearInterval(intervalId);
// 2. Зависший Promise
const promise = new Promise(resolve => {
// resolve НИКОГДА не вызывается
// Promise остаётся в pending состоянии ВЕЧНО
});
// 3. Бесконечная цепочка микротасок
function infiniteMicrotasks() {
Promise.resolve().then(() => infiniteMicrotasks());
}
infiniteMicrotasks();
// Браузер будет делать микротаски ВЕЧНО, никогда не выполнит макротаски
Практический мониторинг
// Можно создать функцию для отладки
function isEventLoopBusy() {
const start = performance.now();
return new Promise(resolve => {
setTimeout(() => {
const end = performance.now();
const delay = end - start;
resolve({
isBusy: delay > 10, // Если setTimeout выполнен с задержкой > 10ms
actualDelay: delay
});
}, 0);
});
}
// Использование
const result = await isEventLoopBusy();
if (result.isBusy) {
console.log('Event loop занят!');
}
Выводы
Браузер определяет, что асинхронный код НЕ выполняется, когда:
- Call stack пуст — нет синхронного кода в стеке вызовов
- Task queue пуста — нет макротасок (setTimeout, fetch callbacks)
- Microtask queue пуста — нет микротасок (Promise, queueMicrotask)
- Нет pending операций — все fetch, XMLHttpRequest завершены или отменены
- Браузер перейдёт в режим ожидания (idle state)
Этот механизм критичен для:
- Оптимизации производительности
- Отладки зависаний приложения
- Управления нагрузкой на CPU
- Обработки пользовательских событий
Основной вывод: браузер отслеживает все очереди и операции в реальном времени, позволяя эффективно управлять асинхронным кодом.