Использование async/await с циклом forEach
Есть ли проблемы с использованием async
/await
в цикле forEach
? Я пытаюсь пройтись по массиву файлов и использовать await
для получения содержимого каждого файла.
import fs from 'fs-promise'
async function printFiles () {
const files = await getFilePaths() // Предполагаем, что это работает нормально
files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
})
}
printFiles()
Этот код работает, но может ли что-то пойти не так? Мне сказали, что не рекомендуется использовать async
/await
в функциях высшего порядка, поэтому я хотел бы узнать, есть ли какие-либо проблемы с такой реализацией.
5 ответ(ов)
Конечно, ваш код работает, но я уверен, что он не делает то, что вы ожидаете. Он просто запускает несколько асинхронных вызовов, а функция printFiles
сразу же возвращает после этого.
Чтение по очереди
Если вы хотите читать файлы последовательно, действительно, нельзя использовать forEach
. Вместо этого просто используйте современный цикл for … of
, в котором await
будет работать как ожидается:
async function printFiles() {
const files = await getFilePaths();
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
Чтение параллельно
Если вы хотите читать файлы параллельно, действительно, нельзя использовать forEach
. Каждая из функций обратного вызова async
возвращает промис, но вы их игнорируете вместо того, чтобы ожидать. Просто используйте map
и вы сможете ожидать массив промисов, который вы получите с помощью Promise.all
:
async function printFiles() {
const files = await getFilePaths();
await Promise.all(files.map(async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}));
}
Вместо использования Promise.all
вместе с Array.prototype.map
(что не гарантирует порядок разрешения Promise
), я применяю Array.prototype.reduce
, начиная с уже разрешенного Promise
:
async function printFiles () {
const files = await getFilePaths();
await files.reduce(async (promise, file) => {
// Эта строка будет ждать завершения последней асинхронной функции.
// В первой итерации используется уже разрешенный Promise,
// так что выполнение продолжится сразу.
await promise;
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}, Promise.resolve());
}
Таким образом, данный подход гарантирует, что файлы будут считываться и выводиться в порядке их следования в массиве. Каждый последующий вызов readFile
будет ожидать завершения предыдущего, что обеспечивает необходимую последовательность выполнения.
Проблема заключается в том, что возвращаемое промисом значение из функции итерации игнорируется методом forEach()
. Он не дожидается завершения выполнения асинхронного кода перед переходом к следующей итерации. Все вызовы fs.readFile
будут выполнены в одном цикле событий, что означает, что они запускаются параллельно, а не последовательно. Выполнение продолжается немедленно после вызова forEach()
, без ожидания завершения всех операций fs.readFile
. Поскольку forEach
не ждет разрешения каждого промиса, цикл фактически завершает итерацию до того, как промисы будут разрешены. Вы ожидаете, что после завершения forEach
весь асинхронный код уже выполнен, но это не так. В результате вы можете попытаться получить доступ к значениям, которые еще недоступны.
Вы можете протестировать это поведение на следующем примере:
const array = [1, 2, 3];
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const delayedSquare = (num) => sleep(100).then(() => num * num);
const testForEach = (numbersArray) => {
const store = [];
// Этот код здесь рассматривается как синхронный
numbersArray.forEach(async (num) => {
const squaredNum = await delayedSquare(num);
// Здесь будет корректное значение squaredNum
// console.log(squaredNum) будет выведен после console.log("store",store)
console.log(squaredNum);
store.push(squaredNum);
});
// Вы ожидаете, что массив store будет заполнен значениями [1,4,9], но это не так
// это вернет []
console.log("store",store);
};
testForEach(array);
// Обратите внимание, что при тестировании сначала будет выведено "store []"
// затем squaredNum внутри forEach будет выведен
Решением этой проблемы является использование цикла for...of
:
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
}
Таким образом, каждый вызов fs.readFile
будет выполнен последовательно, и выполнение кода будет ожидать завершения асинхронных операций перед переходом к следующей итерации.
Вот несколько прототипов для forEachAsync
. Обратите внимание, что вам нужно будет использовать await
с ними:
Array.prototype.forEachAsync = async function (fn) {
for (let t of this) { await fn(t) }
}
Array.prototype.forEachAsyncParallel = async function (fn) {
await Promise.all(this.map(fn));
}
Примечание: хотя вы можете включить это в свой код, не следует добавлять такие методы в библиотеки, которые вы распространяете, чтобы не загрязнять глобальную область видимости других разработчиков.
Для использования:
await myArray.forEachAsyncParallel(async (item) => { await myAsyncFunction(item) })
Учтите, что вам понадобится современный инструмент, такой как esrun
, для использования await
на верхнем уровне в TypeScript.
Это решение также оптимизировано по памяти, поэтому вы сможете запустить его на десятках тысяч элементов данных и запросов. Некоторые другие решения из этого обсуждения могут привести к сбоям сервера при работе с большими наборами данных.
На TypeScript:
export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index);
}
}
Как использовать?
await asyncForEach(receipts, async (eachItem) => {
await ...
});
Таким образом, с помощью функции asyncForEach
вы можете безопасно обрабатывать асинхронные операции для каждого элемента массива, что делает её хорошим выбором для работы с большими массивами данных без риска превышения лимита памяти.
Синтаксис асинхронной стрелочной функции
Запись в файлы в Node.js
Функция map для объектов (вместо массивов)
Как получить полный объект в console.log() Node.js, а не '[Object]'?
Как прочитать JSON-файл в память сервера?