Learning Book

Синхронный vs асинхронный код

Почему JavaScript однопоточный

JavaScript создавался для браузера — добавлять интерактивность на веб-страницы. Один поток означает, что не нужно думать о блокировках, мьютексах и гонках данных. Любой код выполняется от начала до конца без прерывания другим кодом — это свойство называется run-to-completion.

let x = 1
x = x + 1
console.log(x) // 2 — гарантированно, никто не изменит x между строками

Один поток — это просто. Но есть проблема.

Блокирующий код

Если операция занимает время (сетевой запрос, чтение файла, тяжёлые вычисления), весь поток стоит и ждёт:

// ❌ Блокирующий подход (псевдокод)
const data = fetchSync('/api/users')  // 2 секунды ожидания
// Всё это время: UI заморожен, кнопки не нажимаются, анимации стоят
renderUsers(data)

В браузере JavaScript работает в одном потоке с UI — блокирующий код замораживает весь интерфейс. Пользователь видит «зависшую» страницу.

Асинхронный подход

Вместо ожидания JavaScript делегирует долгую операцию среде выполнения (браузеру или Node.js) и продолжает работу:

// ✅ Асинхронный подход
fetch('/api/users')           // отправляем запрос — не ждём
  .then(r => r.json())        // когда ответ придёт — обработаем
  .then(data => renderUsers(data))

console.log('Страница работает!') // выполняется сразу, не ждёт fetch

Ключевая идея: не блокировать поток. Запустил операцию → занимайся другим → получи результат когда готов.

Аналогия с кофейней

Представь кофейню с одним бариста:

Синхронный подход: бариста принимает заказ, делает кофе, отдаёт, и только потом берёт следующий заказ. Очередь стоит.

Асинхронный подход: бариста принимает заказ, ставит кофемашину, пока кофе готовится — принимает следующий заказ. Когда кофе готов — отдаёт. Один бариста обслуживает много клиентов.

JavaScript — этот бариста. Один поток, но благодаря делегированию работы (Web API, libuv) он не простаивает.

Что делает среда выполнения

JavaScript сам по себе не умеет делать сетевые запросы или читать файлы. Это делает среда выполнения:

  • Браузер — предоставляет Web API: fetch, setTimeout, addEventListener, DOM
  • Node.js — предоставляет свои API: fs, http, net, основанные на библиотеке libuv

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

Эволюция асинхронного кода в JS

// 1. Callbacks (1995+)
readFile('data.txt', function(err, data) {
  if (err) handleError(err)
  parseData(data, function(err, result) {
    if (err) handleError(err)
    // ... и так далее — "callback hell"
  })
})

// 2. Promises (ES2015)
readFile('data.txt')
  .then(data => parseData(data))
  .then(result => display(result))
  .catch(err => handleError(err))

// 3. async/await (ES2017)
try {
  const data = await readFile('data.txt')
  const result = await parseData(data)
  display(result)
} catch (err) {
  handleError(err)
}

Каждый шаг — синтаксический сахар над предыдущим. Под капотом всё это работает на event loop — его мы разберём в следующем разделе.

Итого

ФактОписание
ОднопоточностьОдин поток, run-to-completion, нет гонок данных
ПроблемаБлокирующий код замораживает UI
РешениеДелегирование долгих операций среде выполнения
Среда выполненияБраузер (Web API) или Node.js (libuv) — выполняют I/O за пределами потока JS
ЭволюцияCallbacks → Promises → async/await