Асинхронность: однопоточность, call stack, event loop
Однопоточность: почему JavaScript не может делать несколько дел одновременно? 🤔
JavaScript — однопоточный язык. Это значит, что он выполняет команды последовательно, а не параллельно. Представьте очередь в кафе: даже если заказ долгий (например, приготовить латте), следующий ждёт, пока первый не будет готов.
Но как тогда работает асинхронность? За это отвечает Event Loop — механизм, который имитирует параллельность, хотя на самом деле просто ловко переключает задачи.
console.log("Start"); // 1️⃣ Синхронный код
setTimeout(() => {
console.log("Timeout"); // 3️⃣ Асинхронный код
}, 0);
console.log("End"); // 2️⃣ Синхронный код
// Вывод: Start → End → Timeout
Call Stack: где хранятся ваши команды? 🥞
Call Stack — это стек вызовов функций. Когда функция вызывается, она добавляется в стек, а когда завершается — удаляется. JavaScript обрабатывает его до пустого состояния.
Пример:
function greet() {
console.log("Привет!");
}
function meet() {
greet();
console.log("Как дела?");
}
meet();
// Вывод: Привет! → Как дела?
meet()попадает в стек.- Внутри
meet()вызываетсяgreet()— она добавляется поверх. - После выполнения
greet()удаляется, управление возвращается кmeet().
Event Loop: диспетчер асинхронности 🔄
Когда встречается асинхронная операция (например, setTimeout или запрос к серверу), она не блокирует Call Stack. Вместо этого:
- Операция передаётся в Web API (если это браузер) или C++ API (если Node.js).
- После завершения (например, истёк таймер) колбэк попадает в Callback Queue (очередь).
- Event Loop постоянно проверяет, пуст ли Call Stack. Если да — переносит колбэк из очереди в стек.
console.log("Script start");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
fetch("https://api.example.com").then(() => {
console.log("Fetch complete");
});
console.log("Script end");
// Порядок вывода может варьироваться!
Микро- и макрозадачи: кто важнее? ⚖️
Асинхронные задачи делятся на два типа:
- Микрозадачи (Promise, queueMicrotask, process.nextTick в Node.js) — выполняются сразу после текущего синхронного кода, даже перед рендерингом.
- Макрозадачи (setTimeout, setInterval, I/O) — ждут следующего цикла Event Loop.
console.log("Start");
setTimeout(() => console.log("Timeout"), 0); // Макрозадача
Promise.resolve().then(() => console.log("Promise")); // Микрозадача
console.log("End");
// Вывод: Start → End → Promise → Timeout
Как это влияет на реальный код? 🛠️
Пример проблемы: если долгая операция выполняется синхронно (например, сортировка большого массива), интерфейс «зависает». Решение — вынести тяжёлую логику в микрозадачи или Web Workers.
// Плохо: блокирует интерфейс
function sortHugeArray() {
const bigArray = Array(1000000).fill().map(Math.random);
bigArray.sort(); // Долгая операция
}
// Лучше: разбить на части
function asyncSort() {
const chunkSize = 1000;
let index = 0;
function processChunk() {
const chunk = bigArray.slice(index, index + chunkSize);
chunk.sort();
index += chunkSize;
if (index < bigArray.length) {
setTimeout(processChunk, 0); // Даём браузеру "передохнуть"
}
}
processChunk();
}
Итоги: ключевые моменты 🔑
- JavaScript однопоточный, но асинхронность имитируется через Event Loop.
- Call Stack выполняет синхронный код «до дна».
- Web API обрабатывает асинхронные операции вне основного потока.
- Event Loop переносит готовые колбэки из Callback Queue в Call Stack.
- Микрозадачи приоритетнее макрозадач.
Попробуйте «пошагово» разобрать примеры в debugger’e — это лучший способ понять механизм! 🚀