Как работает асинхронность в Node.js?

«Как работает асинхронность в Node.js?» — вопрос из категории Node.js Core, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Node.js использует однопоточный цикл событий (event loop) на базе libuv для неблокирующей обработки операций ввода-вывода (I/O). Когда встречается асинхронная операция (например, запрос к БД или чтение файла), она передается в системный пул потоков libuv, а основной поток Node.js продолжает выполнять синхронный код. По завершении асинхронной операции её колбэк помещается в соответствующую очередь (например, очередь микрозадач для Promise или очередь макрозадач для таймеров и I/O), и цикл событий выполняет его на следующей итерации, когда стек вызовов пуст.

Ключевые компоненты:

  • Event Loop: Однопоточный цикл, который координирует выполнение колбэков.
  • Libuv: Библиотека на C++, предоставляющая пул потоков для выполнения блокирующих системных операций.
  • Очереди: Разные приоритетные очереди для микрозадач (nextTick, Promises) и макрозадач (setTimeout, I/O).

Пример с async/await (современный подход):

const fs = require('fs').promises;

async function readFileAsync() {
  console.log('Начинаем чтение...');
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('Содержимое файла:', data);
  } catch (err) {
    console.error('Ошибка чтения:', err);
  }
  console.log('Этот лог выполнится после await');
}

readFileAsync();
console.log('Этот лог выполнится ДО завершения чтения файла');

Почему так важно: Эта модель позволяет Node.js эффективно обрабатывать тысячи одновременных соединений с минимальными накладными расходами на потоки, что идеально подходит для I/O-интенсивных приложений, таких как веб-серверы или API.