Работа Promise.all в Node.js: параллельно или последовательно
Пройдите тест, узнайте какой профессии подходите
Быстрый ответ
Promise.all
в Node.js запускает обещания синхронно, не ожидая завершения каждого из них индивидуально. Все промисы начинают выполняться немедленно и завершаются, когда последний из них выполнится или будет отклонён. Вот упрощённый ответ на ваш вопрос.
Взглянем на пример с запросами к базе данных:
let promiseArray = [User.findById(1), User.findById(2), User.findById(3)];
Promise.all(promiseArray).then(users => console.log('Пользователи загрузились быстрее, чем вы успеете сказать "осенняя погода"!'));
Каждый вызов 'findById' исполняется независимо от остальных, что позволяет запросам к базе данных происходить одновременно. Однако результат будет выведен в консоль только после получения всех данных.
Цикл For — старый добрый подход
Для последовательного выполнения промисов можно использовать Array.reduce()
или async/await
в цикле. Пример с использованием reduce()
:
let promiseFactory = [fn1, fn2, fn3]; // Функции, возвращающие промисы
promiseFactory.reduce((prevPromise, nextFn) => prevPromise.then(nextFn), Promise.resolve()).then(result => console.log('Выполнено по очереди. Чувствуете дух приключений?'));
В данном контексте промисы выполняются по очереди. Теперь перейдём к использованию async/await:
async function keepInLine(tasks) {
for (const task of tasks) {
await task(); // Каждая задача ожидает своего завершения, прежде чем начнётся следующая.
}
}
В цикле с использованием await
каждая операция выполняется последовательно и структурированно.
Пойми свой Promise
Промисы активируются сразу после создания
Запомните, что новый промис автоматически запускается на выполнение. К моменту передачи его в Promise.all
, каждый промис уже находится в процессе выполнения.
Promise.allSettled
— терпеливый подход
Если вам необходимо дождаться завершения всех операций, независимо от их успешности или неудачи, примените Promise.allSettled
. Этот метод ожидает, пока каждый промис не будет выполнен или отклонён.
Одновременное против параллельного выполнения
Архитектура Node.js не поддерживает истинную многопоточность из-за своего однопоточного исполнения. Однако Node.js эффективно реализует «одновременное» выполнение задач благодаря циклу событий, который оптимизирует неблокирующие I/O операции.
Глубже в тему
Обработка последовательных операций с общими ресурсами
Когда вам требуется работать с общими ресурсами:
let sharedResources = {};
async function danceInSequence(tasks, resources) {
for (const task of tasks) {
Object.assign(resources, await task(resources)); // Каждая задача имеет возможность обновить общие ресурсы.
}
}
Таким образом, задачи используют и модифицируют общие ресурсы последовательно.
Искусство создания цепочек промисов
При создании цепочек с использованием .then
, важно возвращать новый промис:
promise.then(result => {
return new Promise((resolve, reject) => {
// Здесь происходит ваша асинхронная магия!
});
});
Если вы забудете видеть вернуть промис, это спровоцирует ошибки и непредсказуемое поведение.
Визуализация
Представьте автобусный вокзал с несколькими автобусами (Promise.all
), готовыми отправиться по маршрутам:
Автобус №1 🚌: Доехать за 5 минут
Автобус №2 🚌: Доехать за 10 минут
Автобус №3 🚌: Доехать за 3 минуты
Автобусы отправляются одновременно, но прибывают в разное время:
| Отправление 🚦 | Автобус №1 🚌 | Автобус №2 🚌 | Автобус №3 🚌 |
| :----------: | :-------: | :-------: | :-------: |
| **0 мин** | Начало | Начало | Начало |
| **3 мин** | | | Прибытие |
| **5 мин** | Прибытие | | |
| **10 мин** | | Прибытие | |
Группа пассажиров (результат Promise.all
) отправится дальше только после прибытия последнего автобуса.
Дополнительные особенности
Использование рекурсии для последовательной работы
Решением для динамического списка промисов может стать рекурсия:
async function marchOneByOne(index, tasks) {
if (index >= tasks.length) {
return; // Все задачи выполнены.
}
await tasks[index]();
await marchOneByOne(index + 1, tasks); // Следующая задача.
}
marchOneByOne(0, promiseFactories)
.then(() => console.log("Марш окончен. Пора на заслуженный отдых!"));
Симуляция асинхронных операций
Для симуляции setTimeout
подходит как асинхронная операция:
function pretendPromise(result, delay) {
return new Promise(resolve => setTimeout(() => resolve(result), delay))
.then(() => console.log("Промис выполнен, можем продолжить!"));
}
Таким образом, логику промисов можно проверить без осуществления реальных операций.
Относимся к обработке ошибок серьезно
Не забывайте про блок обработки ошибок для обеспечения стабильности ваших асинхронных операций:
Promise.all(promises)
.then(results => console.log(results))
.catch(error => console.error("Обнаружена проблема: ", error));
Это поможет вашим операциям быть надёжными, даже если что-то пойдет не так.
Полезные материалы
- Promise.all() – JavaScript | MDN — Документация MDN по
Promise.all()
. - The event loop – JavaScript | MDN — Расширенный обзор цикла событий JavaScript.
- JavaScript Promises: an introduction — Вводный материал по промисам JavaScript для глубокого понимания асинхронного программирования.
- Tasks, microtasks, queues and schedules – JakeArchibald.com — Изучение задач и микрозадач JavaScript для понимания асинхронного кода.
- Node.js — The Node.js Event Loop, Timers, and process.nextTick() — Обсуждаем цикл событий, таймеры и
process.nextTick()
в контексте Node.js. - Promise API — Руководство с сравнением
Promise.all
иPromise.allSettled
, объясняющее поведение промисов и предлагающее лучшие практики работы с ними.