Асинхронность и Event Loop
Почему асинхронность
Node.js — однопоточный. Один поток на JavaScript, но операции ввода-вывода выполняются в фоне (через libuv).
Проблема: если блокировать поток — приложение встанет.
Решение: асинхронные операции.
Event Loop
Сердце Node.js. Крутится постоянно, обрабатывая очереди задач.
Фазы:
- Timers —
setTimeout,setInterval. - Pending callbacks — I/O callbacks.
- Idle, prepare — внутренние.
- Poll — новые I/O события.
- Check —
setImmediate. - Close callbacks — закрытие.
Microtasks (промисы) — между фазами.
Синхронный код
console.log('1');
console.log('2');
console.log('3');
// 1, 2, 3
Выполняется по порядку.
setTimeout
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 1, 3, 2
setTimeout(fn, 0) — не «сразу», а после текущего кода.
Очереди
Порядок:
- Синхронный код — первым.
- Microtasks — промисы,
queueMicrotask. - Macrotasks —
setTimeout,setInterval, I/O.
Пример:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1, 4, 3, 2
Microtask (3) раньше macrotask (2).
Callback-стиль
Старый способ:
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Проблема — callback hell:
fs.readFile('a.txt', (err, a) => {
fs.readFile('b.txt', (err, b) => {
fs.readFile('c.txt', (err, c) => {
// пирамида
});
});
});
Промисы
Promise — объект с будущим результатом.
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('готово'), 1000);
});
promise.then(result => console.log(result));
Три состояния:
pending— ждёт.fulfilled— успех.rejected— ошибка.
then / catch / finally
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log('Готово'));
async / await
Современный синтаксис:
async function load() {
try {
const res = await fetch('/api/users');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
} finally {
console.log('Готово');
}
}
await — ждёт промис, возвращает результат.
async — функция всегда возвращает промис.
Параллельно
Promise.all — ждёт все:
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);
Быстрее, чем последовательно.
Проблема: если один упадёт — всё упадёт.
Promise.allSettled — не падает:
const results = await Promise.allSettled([
fetchA(),
fetchB(),
fetchC(),
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
Promise.race — первый завершённый:
const result = await Promise.race([
fetchA(),
timeout(3000),
]);
Promise.any — первый успешный:
const result = await Promise.any([
fetchFromServerA(),
fetchFromServerB(),
fetchFromServerC(),
]);
Последовательно
Когда параллельность не подходит:
for (const id of ids) {
const user = await fetchUser(id);
await saveUser(user);
}
Или:
const users = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
setTimeout / setInterval
setTimeout(() => console.log('через 1 сек'), 1000);
const id = setInterval(() => console.log('каждую сек'), 1000);
clearInterval(id); // остановить
С промисами:
const delay = (ms) => new Promise(r => setTimeout(r, ms));
await delay(1000);
setImmediate
После текущей фазы, до I/O:
setImmediate(() => console.log('immediate'));
Отличие от setTimeout(fn, 0): в главном модуле порядок не определён, внутри I/O — setImmediate первым.
process.nextTick
Перед микротасками:
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
nextTick раньше promise.
⚠️ Не злоупотребляй — может заблокировать Event Loop.
Порядок в примере
console.log('start');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('end');
// start, end, nextTick, promise, timeout/immediate (порядок не гарантирован)
Блокирующий код
⚠️ Синхронный код блокирует Event Loop:
// Плохо — блокирует всё на 5 секунд
const data = fs.readFileSync('huge.txt', 'utf8');
// Хорошо
const data = await fs.readFile('huge.txt', 'utf8');
Тяжёлые вычисления — тоже блокируют:
// Плохо
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Решение: worker threads, child processes, или другой язык.
Worker Threads
Параллельно, отдельный поток:
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
if (isMainThread) {
const worker = new Worker(new URL(import.meta.url));
worker.on('message', result => console.log(result));
} else {
const result = heavyComputation();
parentPort.postMessage(result);
}
Обработка ошибок
В async:
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error('Ошибка:', err.message);
throw err;
}
}
Unhandled rejection:
process.on('unhandledRejection', (reason) => {
console.error('Unhandled:', reason);
});
Uncaught exception:
process.on('uncaughtException', (err) => {
console.error('Uncaught:', err);
process.exit(1);
});
⚠️ После uncaughtException — процесс в неопределённом состоянии. Лучше exit.
Практика: загрузка с retry
async function fetchWithRetry(url, maxAttempts = 3) {
for (let i = 1; i <= maxAttempts; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(`Попытка ${i} провалилась:`, err.message);
if (i === maxAttempts) throw err;
await new Promise(r => setTimeout(r, 1000 * i));
}
}
}
const data = await fetchWithRetry('https://api.example.com/users');
Практика: параллельная загрузка
async function loadAll(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
return results.map((r, i) => ({
url: urls[i],
ok: r.status === 'fulfilled',
data: r.status === 'fulfilled' ? r.value : null,
error: r.status === 'rejected' ? r.reason.message : null,
}));
}
const results = await loadAll([
'https://api.example.com/users',
'https://api.example.com/posts',
]);
console.log(results);
Итог
- Event Loop — сердце Node.js.
- Microtasks раньше macrotasks.
- Callback → Promise → async/await — эволюция.
- Promise.all — параллельно.
- Promise.allSettled — не падает.
- Promise.race / any — первый.
- Не блокируй Event Loop.
- Worker threads — для CPU-heavy.
- Обрабатывай ошибки —
unhandledRejection.
Дальше — HTTP-сервер.