Learn JavaScript

Асинхронность в JavaScript: почему код выполняется не по порядку

Иногда в JavaScript кажется, что код выполняется “не по порядку”: сначала написано одно, а в консоли появляется другое. Это нормально - так работает асинхронность.

Асинхронность нужна, чтобы браузер не “зависал”, пока идет длительная операция: запрос в сеть, ожидание таймера, чтение файла, работа с пользовательским вводом.

1) Синхронный код: строго сверху вниз

Синхронный код выполняется по порядку, строка за строкой.

console.log("A");
console.log("B");
console.log("C");

Результат:

A
B
C

2) Почему нельзя просто “ждать” в JavaScript

JavaScript в браузере обычно выполняется в одном потоке: пока выполняется код, страница не может нормально реагировать на клики, прокрутку, ввод.

Если сделать тяжелую работу синхронно, интерфейс “замрет”.

console.log("Start");

let sum = 0;
for (let i = 0; i < 1_000_000_000; i += 1) {
  sum += i;
}

console.log("End");

Пока идет цикл, страница может подвиснуть.

3) Асинхронные операции: результат будет позже

Некоторые действия запускаются сейчас, а результат приходит позже:

  • таймеры (setTimeout)
  • сетевые запросы (fetch)
  • события (клик, input)
  • работа с файлами

Такие операции обычно принимают функцию, которая выполнится, когда результат готов.

Пример с setTimeout

console.log("A");

setTimeout(() => {
  console.log("B (timeout)");
}, 0);

console.log("C");

Вывод будет:

A
C
B (timeout)

Почему так: setTimeout(..., 0) не означает “сразу”. Это означает “выполнить после того, как завершится текущий синхронный код и очередь позволит”.

4) Что происходит внутри: очередь задач и event loop

Упрощенная модель:

  • синхронный код выполняется прямо сейчас
  • асинхронные коллбеки попадают в очередь
  • когда основной код закончился, event loop берет задачи из очереди и выполняет

Из-за этого и возникает эффект “не по порядку”.

5) Callback как идея: выполнить позже

Обработчик в setTimeout - это callback.

function logLater(text) {
  setTimeout(() => {
    console.log(text);
  }, 500);
}

logLater("Hello");

Смысл: “сделать сейчас, а завершить позже”.

6) Promises: удобный формат “результат будет потом”

Многие современные API (например fetch) возвращают Promise - объект, который представляет будущий результат.

Мини-пример:

const promise = Promise.resolve("OK");

promise.then((value) => {
  console.log(value);
});

console.log("After");

Сначала выведется “After”, затем “OK” (Promise тоже выполняется не в середине синхронного кода).

7) Почему это важно для реального кода

Когда внутри есть запрос или таймер, результат нельзя использовать “сразу” после запуска.

Неправильная логика:

let data;

fetch("/api/data").then((response) => response.json()).then((json) => {
  data = json;
});

console.log(data);

console.log(data) сработает раньше, чем придет ответ, поэтому там будет undefined.

Правильный подход: работать с данными внутри then или через async/await (это будет следующим шагом в модуле).

fetch("/api/data")
  .then((response) => response.json())
  .then((json) => {
    console.log(json);
  });

8) Частые причины “не по порядку”

  • setTimeout, setInterval
  • fetch и любые сетевые запросы
  • события DOM (клик, input)
  • Promise и then
  • async функции (внутри они тоже используют Promise)

Что стоит запомнить

  • Синхронный код идет сверху вниз.
  • Асинхронные операции завершаются позже и ставят продолжение в очередь.
  • setTimeout(..., 0) не выполняется мгновенно, он ждет завершения текущего кода.
  • Promise - стандартный способ работать с будущим результатом.
  • Если результат приходит позже, его нельзя использовать “сразу после запуска” - логика должна быть внутри then или через async/await.