Асинхронность: однопоточность, 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();
// Вывод: Привет! → Как дела?
  1. meet() попадает в стек.
  2. Внутри meet() вызывается greet() — она добавляется поверх.
  3. После выполнения greet() удаляется, управление возвращается к meet().

Event Loop: диспетчер асинхронности 🔄

Когда встречается асинхронная операция (например, setTimeout или запрос к серверу), она не блокирует Call Stack. Вместо этого:

  1. Операция передаётся в Web API (если это браузер) или C++ API (если Node.js).
  2. После завершения (например, истёк таймер) колбэк попадает в Callback Queue (очередь).
  3. 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();
}

Итоги: ключевые моменты 🔑

  1. JavaScript однопоточный, но асинхронность имитируется через Event Loop.
  2. Call Stack выполняет синхронный код «до дна».
  3. Web API обрабатывает асинхронные операции вне основного потока.
  4. Event Loop переносит готовые колбэки из Callback Queue в Call Stack.
  5. Микрозадачи приоритетнее макрозадач.

Попробуйте «пошагово» разобрать примеры в debugger’e — это лучший способ понять механизм! 🚀

Скрыть рекламу навсегда

📘 VK Видео — обучение без ограничений

Все уроки доступны без VPN, без блокировок и зависаний.

Можно смотреть с телефона, планшета или компьютера — в любое время.

▶️ Смотреть на VK Видео