$ sudo teach IT
РАЗДЕЛ 10 · УРОК 1

Синхронность vs асинхронность и Event Loop

JavaScript однопоточный — он делает одно дело за раз. Тогда как он умудряется не зависать, пока грузится картинка или запрос идёт на сервер? Сегодня разберём магию Event Loop — один из самых важных концептов в JS.

⏱ ~25 минут 🎓 Для новичков ⚡ JavaScript

🚶 Что такое синхронный код

Весь код, который вы писали раньше в этом курсе, был синхронным. Это значит: строки выполняются одна за другой, строго по порядку. Текущая строка должна полностью завершиться, прежде чем начнётся следующая.

console.log("Шаг 1");
console.log("Шаг 2");
console.log("Шаг 3");
// Вывод всегда: Шаг 1, Шаг 2, Шаг 3 — строго по порядку

Это предсказуемо и понятно. Но вот проблема — что если какая-то операция занимает долгое время? Например, вы хотите прочитать большой файл с диска, или отправить запрос на сервер и подождать ответ. В синхронном мире всё остальное заблокируется и будет ждать.

🚫 Проблема синхронного кода: представьте, что браузер синхронно ждёт ответа от сервера. Всё — страница не реагирует на клики, нельзя прокрутить, нельзя ввести текст. Браузер «завис». Пользователь закрывает вкладку и никогда не возвращается.

Псевдокод синхронного блокирующего кода выглядел бы так:

// Если бы fetch был синхронным (так НЕ работает в реальности!)
const data = fetchDataFromServer(); // ← всё висит здесь 3 секунды...
console.log("Страница снова работает"); // ← только потом

🍽️ Аналогия: официант в ресторане

Лучшая аналогия для понимания синхронного и асинхронного — работа официанта.

😩
Синхронный официант

Принял заказ у стола 1 → пошёл на кухню → стоит, смотрит как повар готовит → ждёт 20 минут → несёт еду → только теперь идёт к столу 2. Все остальные гости голодают и злятся.

😎
Асинхронный официант

Принял заказ у стола 1 → передал на кухню → не ждёт → идёт к столу 2 → принял заказ → идёт к столу 3... Когда кухня готова — берёт заказ и несёт. Все счастливы!

JavaScript работает как хороший официант. Когда он встречает долгую операцию (запрос к серверу, таймер, чтение файла), он не стоит и не ждёт — он говорит «хорошо, когда будет готово — скажи мне» и идёт дальше выполнять другой код. А когда операция завершается — JavaScript возвращается и обрабатывает результат.

💡 Асинхронность — это способность начать длительную операцию и не блокировать остальной код, пока она выполняется. Когда операция завершится, будет вызвана функция-обработчик (колбэк).

🧵 JavaScript однопоточный — это важно

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

JavaScript так не работает. У него один поток выполнения. В каждый момент времени он делает ровно одно дело. Это было сознательное решение создателей языка — браузерный JS не должен был сталкиваться со сложными проблемами параллельности (race conditions, deadlocks и т.д.).

Подождите — если JavaScript однопоточный, как он вообще может делать что-то асинхронно? Он же не может одновременно ждать ответа от сервера И выполнять другой код, если у него один поток!

Ответ: долгие операции выполняются НЕ в самом JavaScript. Их выполняет браузер (или Node.js) — на своей стороне, отдельно. JavaScript только запускает операцию и говорит «позови меня когда закончишь».

Это ключевое понимание! Когда вы делаете fetch(), браузер берёт эту задачу к себе (в свои Web APIs) и выполняет сетевой запрос самостоятельно. JavaScript при этом свободен и продолжает выполнять другой код. Когда браузер получил ответ — он уведомляет JavaScript через механизм Event Loop.

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

Event Loop — это механизм, который координирует всё это волшебство. Чтобы понять его, нужно разобраться с несколькими компонентами.

📚
Call Stack (стек вызовов)

Стопка функций, которые сейчас выполняются. Вызвал функцию — она легла на верх стека. Функция завершилась — сняли с верха. JavaScript выполняет только то, что на вершине стека.

🌐
Web APIs

Возможности, которые предоставляет браузер: setTimeout, fetch, DOM-события. Когда JS запускает setTimeout — браузер берёт таймер к себе и отсчитывает время. JS об этом не знает и не тратит ресурсы.

📬
Callback Queue (очередь)

Когда Web API завершает работу (таймер истёк, ответ пришёл), колбэк-функция помещается в очередь и ждёт своей очереди на выполнение.

🔁
Event Loop

Постоянно крутится в цикле и проверяет: «стек пустой? Очередь не пустая?» Если да — берёт из очереди следующий колбэк и кладёт его в стек на выполнение.

⚠️ Важно: Event Loop берёт колбэк из очереди только тогда, когда Call Stack полностью пустой. Это означает: если ваш синхронный код работает долго и не отдаёт управление — все события и таймеры будут ждать.

🔍 Разбор на примере: setTimeout

Давайте по шагам разберём, что происходит с этим кодом:

console.log("1: начало");

setTimeout(function() {
  console.log("2: внутри таймера");
}, 0);

console.log("3: конец");

// Что выведет? Угадайте перед тем как читать дальше...

Результат:

1: начало
3: конец
2: внутри таймера

Удивительно? Таймер стоит 0 миллисекунд, но его колбэк выполнился последним! Вот что происходит по шагам:

1

console.log("1: начало") кладётся в стек → выполняется → выводит "1: начало" → убирается из стека.

2

setTimeout(...) кладётся в стек → JS передаёт колбэк браузеру (Web API) и говорит «запусти через 0мс» → setTimeout убирается из стека. Колбэк пока у браузера.

3

console.log("3: конец") кладётся в стек → выполняется → выводит "3: конец" → убирается. Стек пустой.

4

Браузер давно уже отсчитал 0мс и положил колбэк в Callback Queue. Event Loop видит: стек пустой, очередь не пустая → берёт колбэк и кладёт в стек → выводит "2: внутри таймера".

💡 Вывод: setTimeout с задержкой 0мс не значит «выполни прямо сейчас». Это значит «выполни как можно скорее, когда стек освободится». Минимум — после текущего синхронного кода.

🛠️ Зачем это знать на практике

Понимание Event Loop помогает избежать очень распространённых ошибок. Вот классический пример:

// Частая ошибка новичков
let result;

fetch("https://api.example.com/data")
  .then(res => res.json())
  .then(data => {
    result = data; // записываем результат
  });

console.log(result); // undefined! Почему?

Потому что fetch асинхронный. Пока идёт запрос, JavaScript дошёл до console.log(result) и выполнил его — а результат ещё не пришёл! Переменная result всё ещё undefined.

Правильный способ — работать с результатом внутри колбэка или использовать async/await (об этом в уроке 10.4):

fetch("https://api.example.com/data")
  .then(res => res.json())
  .then(data => {
    console.log(data); // ✅ здесь данные точно есть
    // вся работа с результатом — внутри .then()
  });

Также важно понимать: никогда не блокируйте Event Loop тяжёлыми синхронными операциями. Например, такой цикл заморозит страницу:

// Плохо! Замораживает браузер
for (let i = 0; i < 1000000000; i++) {
  // что-то тяжёлое
}
// За всё это время ни один клик, ни один таймер не обработается

🔬 Бонус: Microtask Queue (для любопытных)

На самом деле очередей две. Есть Callback Queue (или Macrotask Queue) — туда попадают setTimeout, setInterval, события DOM. И есть Microtask Queue — туда попадают промисы (.then()) и queueMicrotask().

Event Loop обрабатывает все микрозадачи перед тем, как взять следующую макрозадачу. Поэтому промисы выполняются быстрее таймеров:

console.log("1");

setTimeout(() => console.log("2: setTimeout"), 0);

Promise.resolve().then(() => console.log("3: Promise"));

console.log("4");

// Вывод:
// 1
// 4
// 3: Promise   ← микрозадача — раньше таймера!
// 2: setTimeout ← макрозадача — позже

📌 Это объяснение для полноты картины. На практике нужно просто знать: промисы выполняются раньше таймеров, если оба ждут. Детали важны при глубоком изучении JS.

✅ Итоги урока

  • Синхронный код выполняется строка за строкой и блокирует всё остальное
  • Асинхронный код позволяет запустить долгую операцию и не ждать — продолжать работу
  • JavaScript однопоточный, но долгие операции выполняются браузером (Web APIs) отдельно
  • Event Loop — цикл, который перекладывает завершённые колбэки из очереди в стек вызовов
  • Порядок: Call Stack → Web APIs → Callback Queue → Event Loop → Call Stack
  • setTimeout(fn, 0) НЕ значит «прямо сейчас» — колбэк выполнится только когда стек пустой
  • Никогда не блокируйте Event Loop тяжёлыми синхронными вычислениями

В следующем уроке познакомимся с setTimeout и setInterval подробно, научимся делать таймеры и разберём проблему «callback hell». 🚀

Синхронность vs асинхронность, Event Loop

8 вопросов

Порядок вывода асинхронного кода

Дан код: console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C'); Почему B выводится последним, если задержка 0? Напишите порядок вывода.

Microtask vs Macrotask

Дан код: console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); Объясните, почему промис (3) выводится раньше таймера (2). Напишите порядок вывода.