Какие знаешь способы работы с асинхронным кодом?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы работы с асинхронным кодом в JavaScript/Node.js
Асинхронное программирование — основа Node.js. Я использовал разные подходы в зависимости от задачи и эпохи развития проекта.
1. Callbacks (устаревший способ)
Самый ранний подход — передача функции как аргумента:
// Старомодный callback hell
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
db.query('SELECT * FROM users', (err, results) => {
if (err) throw err;
const processed = results.map(r => r.name);
fs.writeFile('output.txt', processed, (err) => {
if (err) throw err;
console.log('Done');
});
});
});
// Проблемы:
// - Callback hell (пирамида doom)
// - Сложно обработать ошибки
// - Трудно читать и поддерживать код
Использовал в legacy коде, но больше не рекомендую.
2. Promises (современный подход)
Promises — объект, представляющий будущее значение:
// Создание Promise
const getUser = (id) => {
return new Promise((resolve, reject) => {
db.query(`SELECT * FROM users WHERE id = $1`, [id], (err, result) => {
if (err) reject(err);
else resolve(result[0]);
});
});
};
// Использование
getUser(1)
.then(user => {
console.log('User:', user);
return fs.promises.writeFile('user.json', JSON.stringify(user));
})
.then(() => console.log('File saved'))
.catch(err => console.error('Error:', err))
.finally(() => console.log('Done'));
// Promise.all — параллельные операции
Promise.all([
getUser(1),
getUser(2),
getUser(3)
])
.then(users => console.log(users))
.catch(err => console.error(err));
// Promise.race — первое завершённое
Promise.race([
getUser(1),
timeout(5000) // Timeout race condition
])
.then(result => console.log(result))
.catch(err => console.error('Timeout or error'));
// Promise.allSettled — все результаты (даже ошибки)
Promise.allSettled([
getUser(1),
getUser(999), // Может ошибиться
getUser(2)
])
.then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(i, result.value);
} else {
console.log(i, 'Error:', result.reason);
}
});
});
Отлично подходит для большинства случаев. Все современные API на Promises.
3. async/await (лучший способ)
Синтаксический сахар над Promises для более читаемого кода:
// async функция всегда возвращает Promise
async function main() {
try {
// await паузит выполнение до разрешения Promise
const user = await getUser(1);
console.log('User:', user);
// Последовательные операции
const file = await fs.promises.writeFile('user.json', JSON.stringify(user));
console.log('File saved');
} catch (err) {
console.error('Error:', err);
} finally {
console.log('Done');
}
}
main();
// Параллельные операции с async/await
async function getMultipleUsers() {
try {
// Неправильно — последовательно
const user1 = await getUser(1);
const user2 = await getUser(2);
const user3 = await getUser(3);
// Занимает: 3x времени
// Правильно — параллельно
const [user1, user2, user3] = await Promise.all([
getUser(1),
getUser(2),
getUser(3)
]);
// Занимает: 1x времени
} catch (err) {
console.error(err);
}
}
// Обработка ошибок
async function robustOperation() {
try {
const result = await risky();
return result;
} catch (err) {
if (err.code === 'ENOENT') {
// Файл не найден
return null;
}
throw err; // Пробросить дальше
}
}
Это мой любимый способ — читаемый, простой в отладке.
4. Event Emitters
Для работы с событиями (особенно в потоках):
const EventEmitter = require('events');
class DataProcessor extends EventEmitter {
async process(data) {
this.emit('start', { timestamp: Date.now() });
try {
const result = await doSomething(data);
this.emit('success', result);
} catch (err) {
this.emit('error', err);
}
}
}
// Использование
const processor = new DataProcessor();
processor.on('start', (info) => {
console.log('Processing started:', info);
});
processor.on('success', (result) => {
console.log('Success:', result);
});
processor.on('error', (err) => {
console.error('Error:', err);
});
await processor.process({ id: 1 });
Использовал для стриминга и long-running операций.
5. Streams (для больших данных)
Streams обрабатывают данные по кускам:
const fs = require('fs');
// Чтение большого файла потоком (экономит память)
fs.createReadStream('large-file.txt')
.pipe(transformation) // Трансформирование
.pipe(fs.createWriteStream('output.txt'))
.on('finish', () => console.log('Done'))
.on('error', (err) => console.error(err));
// Или через pipeline (более безопасный способ)
const { pipeline } = require('stream');
pipeline(
fs.createReadStream('input.txt'),
transformation,
fs.createWriteStream('output.txt'),
(err) => {
if (err) console.error('Pipeline error:', err);
else console.log('Pipeline done');
}
);
// Promise-based pipeline (Node 15+)
const { pipeline } = require('stream/promises');
await pipeline(
fs.createReadStream('input.txt'),
transformation,
fs.createWriteStream('output.txt')
);
Критично для работы с большими файлами, видео стриминга.
6. Worker Threads
Для CPU-intensive операций (не блокирующие event loop):
const { Worker } = require('worker_threads');
// worker.js
if (require('worker_threads').isMainThread) {
// Главный поток
const worker = new Worker(__filename);
worker.on('message', (result) => {
console.log('Result:', result);
});
worker.postMessage({ n: 10 });
} else {
// Worker поток
const { parentPort } = require('worker_threads');
parentPort.on('message', (msg) => {
const result = fibonacci(msg.n);
parentPort.postMessage(result);
});
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Использовал для обработки изображений, шифрования.
7. Generators и async generators
Более редкий подход, но полезный:
// Generator функция
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
const gen = generateNumbers();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// Async generator
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
// Использование
for await (const value of asyncGenerator()) {
console.log(value);
}
8. Библиотеки для асинхрона
RxJS — Reactive Extensions (сложный, но мощный):
import { interval, of } from 'rxjs';
import { map, filter, switchMap } from 'rxjs/operators';
interval(1000)
.pipe(
filter(x => x % 2 === 0),
map(x => x * 2),
switchMap(x => of(x))
)
.subscribe(console.log);
Bluebird — Promise утилиты (устаревает):
const Promise = require('bluebird');
const fs = require('fs');
// Promisify callback API
const readFile = Promise.promisify(fs.readFile);
const content = await readFile('file.txt');
p-queue — управление параллельными операциями:
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 2 });
await queue.add(() => getUser(1));
await queue.add(() => getUser(2));
await queue.add(() => getUser(3));
Сравнение методов
| Метод | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
| Callbacks | Простой, нет overhead | Callback hell | legacy код |
| Promises | Мощный, хороший контроль | Многословный | legacy API |
| async/await | Читаемый, простой | Новичков путает | ВСЕГДА |
| Streams | Эффективен для больших данных | Сложный API | файлы, video |
| Workers | Не блокирует event loop | Overhead на создание | тяжелые вычисления |
| RxJS | Мощный для реактивности | Steep learning curve | real-time приложения |
На практике
На production использую в основном async/await с Promise.all для параллелизма. Для специальных случаев:
- Streams — обработка больших файлов
- Worker Threads — CPU-heavy операции
- Event Emitters — архитектура на событиях (pub/sub)
- RxJS — real-time приложения (WebSockets, live updates)
Главное правило: всегда обрабатывай ошибки и избегай race conditions!