Використання async / wait за допомогою циклу forEach


1128

Чи є проблеми з використанням async/ awaitу forEachциклі? Я намагаюся прокрутити масив файлів та awaitвміст кожного файлу.

import fs from 'fs-promise'

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
}

printFiles()

Цей код працює, але чи може щось із цим піти не так? У мене хтось сказав мені, що ти не повинен використовувати async/ awaitяк функцію вищого порядку, як це, тому я просто хотів запитати, чи є проблема з цим.

Відповіді:


2144

Звичайно, код працює, але я впевнений, що він не буде робити те, що ви очікуєте. Він просто запускає кілька асинхронних дзвінків, але 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)
  }));
}

33
Чи можете ви поясніть, чому це for ... of ...працює?
Demonbane

84
Гаразд, я знаю, чому ... Використання Babel перетворює функцію async/ awaitна генератор та використовує forEachзасоби, що кожна ітерація має індивідуальну функцію генератора, яка не має нічого спільного з іншими. тому вони будуть виконуватися незалежно і не мають контексту для next()інших. Насправді, простий for()цикл також працює, оскільки ітерації також знаходяться в одній генераторній функції.
Demonbane

21
@Demonbane: Словом, тому що він був розроблений для роботи :-) awaitпризупиняє поточну оцінку функцій , включаючи всі структури управління. Так, він досить схожий на генератори в цьому плані (саме тому вони використовуються для поліфінування асинхронізації / очікування).
Бергі

3
@ arve0 Насправді asyncфункція сильно відрізняється від Promiseзворотного виклику виконавця, але так, mapзворотний виклик повертає обіцянку в обох випадках.
Бергі

5
Коли ви приїдете дізнатися про обіцянки JS, але замість цього використовуйте півгодини перекладу латиницею. Сподіваюся, ви пишаєтесь @Bergi;)
Фелікс Ганьон-Греньє

188

Завдяки ES2018 ви зможете значно спростити всі перераховані вище відповіді на:

async function printFiles () {
  const files = await getFilePaths()

  for await (const file of fs.readFile(file, 'utf8')) {
    console.log(contents)
  }
}

Дивіться специфікацію: пропозиція-асинхронізація


2018-09-10: Ця відповідь останнім часом привертає велику увагу, перегляньте публікацію в блозі Акселя Раушмайєра для отримання додаткової інформації про асинхронну ітерацію: ES2018: асинхронна ітерація


4
Запропоновано, було б чудово, якби ви могли поставити посилання на специфікацію у своїй відповіді для всіх, хто хоче дізнатися більше про ітерацію асинхронізації.
saadq

8
Чи не повинен це вміст замість файлу в ітераторі
FluffyBeing

10
Чому люди підтримують цю відповідь? Роздивіться детальніше відповідь, запитання та пропозицію. Після цього ofповинна бути функція async, яка поверне масив. Франциско сказав:
Євгеній Герасимчук

3
Цілком погоджуюся з @AntonioVal. Це не відповідь.
Євгеній Герасимчук

2
Хоча я погоджуюся, що це не відповідь, оскарження пропозиції - це спосіб підвищити її популярність, потенційно зробивши її доступною для використання раніше.
Роберт Моліна

61

Замість Promise.allпоєднання з Array.prototype.map(що не гарантує порядок вирішення Promises), я використовую Array.prototype.reduce, починаючи з дозволеного Promise:

async function printFiles () {
  const files = await getFilePaths();

  await files.reduce(async (promise, file) => {
    // This line will wait for the last async function to finish.
    // The first iteration uses an already resolved Promise
    // so, it will immediately continue.
    await promise;
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }, Promise.resolve());
}

1
Це прекрасно працює, дуже дякую. Чи не могли б ви пояснити , що відбувається тут з Promise.resolve()і await promise;?
parrker9

1
Це досить класно. Я маю рацію, думаючи, що файли будуть прочитані по порядку, а не всі відразу?
GollyJer

1
@ parrker9 Promise.resolve()повертає вже вирішений Promiseоб’єкт, так що reduceвін Promiseповинен почати з. await promise;буде чекати останнього Promiseв ланцюзі, щоб вирішити. @GollyJer Файли будуть оброблятися послідовно, по одному.
Тимофій Зорн

Дуже круте використання скорочення, дякую за коментар! Я просто зазначу, що на відміну від деяких інших методів, згаданих у коментарях, цей метод є синхронним, тобто файли читаються один за одним, а не паралельно (оскільки наступна ітерація функції зменшення покладається на попередню ітерація, вона повинна бути синхронною).
Шай Єжаков

1
@Shay, ти маєш на увазі послідовну, а не синхронну. Це все ще асинхронно - якщо інші речі заплановані, вони будуть працювати між ітераціями тут.
Тимофій Зорн

32

Модуль p-ітерації в npm реалізує методи ітерації Array, щоб їх можна було використовувати дуже просто з async / wait.

Приклад вашої справи:

const { forEach } = require('p-iteration');
const fs = require('fs-promise');

(async function printFiles () {
  const files = await getFilePaths();

  await forEach(files, async (file) => {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  });
})();

1
Мені це подобається, оскільки він має ті ж функції / методи, що й сам JS - в моєму випадку мені це потрібно, someа не forEach. Дякую!
mikemaccana

25

Ось кілька 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));
}

Зауважте, хоча ви можете включати це у свій власний код, не слід включати це в бібліотеки, які ви поширюєте іншим (щоб уникнути забруднення їх глобальних точок).


1
Хоча я б вагався додати речі безпосередньо до прототипу, це приємна асинхроніка для кожної реалізації
DaniOcean

2
Поки назва в майбутньому є унікальною (як я б і використовував _forEachAsync), це розумно. Я також думаю, що це найкраща відповідь, оскільки це економить багато кодового коду.
mikemaccana

1
@estus Це дозволяє уникнути забруднення коду інших людей. Якщо код належить нашій особистій організації, а глобальні знаходяться у чітко визначеному файлі ( globals.jsбуло б добре), ми можемо додати глобали, як нам подобається.
mikemaccana

1
@mikemaccana Це уникає загальноприйнятих поганих практик. Це правда, це можна зробити до тих пір, поки ви використовуєте лише код першої сторони, що трапляється рідко. Проблема полягає в тому, що, коли ви використовуєте сторонні лібри, може бути якийсь інший хлопець, який почуває себе так само і модифікує ті самі глобалі, тільки тому, що це здавалося гарною ідеєю в той час, коли ліб був написаний.
колба

1
@estus Звичайно. Я додав попередження до питання, щоб зберегти тут (не особливо продуктивну) дискусію.
mikemaccana

6

Окрім відповіді @ Бергі , я хотів би запропонувати і третю альтернативу. Це дуже схоже на другий приклад @ Бергі, але замість того, щоб чекати кожного readFileокремо, ви створюєте масив обіцянок, кожне з яких ви очікуєте в кінці.

import fs from 'fs-promise';
async function printFiles () {
  const files = await getFilePaths();

  const promises = files.map((file) => fs.readFile(file, 'utf8'))

  const contents = await Promise.all(promises)

  contents.forEach(console.log);
}

Зауважте, що переданій функції .map()не потрібно бути async, оскільки fs.readFileповертає об'єкт Promise так чи інакше. Тому promisesце масив об'єктів Promise, на який можна надіслати Promise.all().

У відповіді @ Бергі консоль може записувати вміст файлів у порядку, який вони читають. Наприклад, якщо дійсно невеликий файл закінчує читання перед дійсно великим файлом, він буде реєструватися спочатку, навіть якщо маленький файл надходить після великого файлу в filesмасиві. Однак у моєму вищевикладеному способі ви гарантовано консолі записуєте файли в тому ж порядку, що і наданий масив.


1
Я впевнений, що ви невірні: я майже впевнений, що ваш метод також може читати файли в порядку. Так, він запише висновок у правильному порядку (завдяки await Promise.all), але файли, можливо, були прочитані в іншому порядку, що суперечить вашому твердженню "Ви гарантовано консоль записуватиме файли в тому ж порядку, що і вони" читати ".
Венрікс

1
@Venryx Ви праві, дякую за виправлення. Я оновив свою відповідь.
chharvey

5

Рішення Берджі прекрасно працює, коли fsзасновано на обіцянках. Ви можете використовувати bluebird, fs-extraабо fs-promiseдля цього.

Однак рішення для рідної fsбібліотеки вузла полягає в наступному:

const result = await Promise.all(filePaths
    .map( async filePath => {
      const fileContents = await getAssetFromCache(filePath, async function() {

        // 1. Wrap with Promise    
        // 2. Return the result of the Promise
        return await new Promise((res, rej) => {
          fs.readFile(filePath, 'utf8', function(err, data) {
            if (data) {
              res(data);
            }
          });
        });
      });

      return fileContents;
    }));

Примітка: require('fs') примусово приймає функцію як треті аргументи, інакше видає помилку:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

4

Обидва рішення над роботою, однак, "Антоніо" виконує роботу з меншим кодом, ось, як це допомогло мені вирішити дані з моєї бази даних, з декількох різних дочірніх записів, а потім підштовхнувши їх до масиву і вирішивши це в обіцянці, зрештою, це зроблено:

Promise.all(PacksList.map((pack)=>{
    return fireBaseRef.child(pack.folderPath).once('value',(snap)=>{
        snap.forEach( childSnap => {
            const file = childSnap.val()
            file.id = childSnap.key;
            allItems.push( file )
        })
    })
})).then(()=>store.dispatch( actions.allMockupItems(allItems)))

3

досить безболісно проскакувати пару методів у файлі, які будуть обробляти асинхронні дані в серіалізованому порядку і надавати більш звичайний смак вашому коду. Наприклад:

module.exports = function () {
  var self = this;

  this.each = async (items, fn) => {
    if (items && items.length) {
      await Promise.all(
        items.map(async (item) => {
          await fn(item);
        }));
    }
  };

  this.reduce = async (items, fn, initialValue) => {
    await self.each(
      items, async (item) => {
        initialValue = await fn(initialValue, item);
      });
    return initialValue;
  };
};

Тепер, припускаючи, що це збережено в './myAsync.js', ви можете зробити щось подібне до наведеного нижче у суміжному файлі:

...
/* your server setup here */
...
var MyAsync = require('./myAsync');
var Cat = require('./models/Cat');
var Doje = require('./models/Doje');
var example = async () => {
  var myAsync = new MyAsync();
  var doje = await Doje.findOne({ name: 'Doje', noises: [] }).save();
  var cleanParams = [];

  // FOR EACH EXAMPLE
  await myAsync.each(['bork', 'concern', 'heck'], 
    async (elem) => {
      if (elem !== 'heck') {
        await doje.update({ $push: { 'noises': elem }});
      }
    });

  var cat = await Cat.findOne({ name: 'Nyan' });

  // REDUCE EXAMPLE
  var friendsOfNyanCat = await myAsync.reduce(cat.friends,
    async (catArray, friendId) => {
      var friend = await Friend.findById(friendId);
      if (friend.name !== 'Long cat') {
        catArray.push(friend.name);
      }
    }, []);
  // Assuming Long Cat was a friend of Nyan Cat...
  assert(friendsOfNyanCat.length === (cat.friends.length - 1));
}

2
Невеликий додаток, не забудьте обернути очікувані / асинкси в блоки спробувати / ловити !!
Джей Едвардс

3

Наче відповідь @ Бергі, але з однією різницею.

Promise.all відхиляє всі обіцянки, якщо когось відхиляють.

Отже, використовуйте рекурсію.

const readFilesQueue = async (files, index = 0) {
    const contents = await fs.readFile(files[index], 'utf8')
    console.log(contents)

    return files.length <= index
        ? readFilesQueue(files, ++index)
        : files

}

const printFiles async = () => {
    const files = await getFilePaths();
    const printContents = await readFilesQueue(files)

    return printContents
}

printFiles()

PS

readFilesQueueзнаходиться поза printFilesпричиною побічного ефекту *, запровадженого компанією console.log, краще знущатися, перевіряти чи шпигувати, тому не круто мати функцію, яка повертає вміст (sidenote).

Таким чином, код може бути спроектований таким чином: три розділені функції, які є "чистими" ** і не вводять жодних побічних ефектів, обробляють весь список і можуть бути легко модифіковані для обробки несправних випадків.

const files = await getFilesPath()

const printFile = async (file) => {
    const content = await fs.readFile(file, 'utf8')
    console.log(content)
}

const readFiles = async = (files, index = 0) => {
    await printFile(files[index])

    return files.lengh <= index
        ? readFiles(files, ++index)
        : files
}

readFiles(files)

Майбутнє редагування / поточний стан

Вузол підтримує найвищий рівень очікування (у нього ще немає плагіна, не буде і його можна включити за допомогою прапорів гармонії), це здорово, але не вирішує жодної проблеми (стратегічно я працюю лише у версіях LTS). Як отримати файли?

Використання композиції. Враховуючи код, викликає в мене відчуття, що це всередині модуля, тож має функціонувати це. Якщо ні, то ви повинні використовувати IIFE, щоб перетворити код ролі у функцію асинхронізації, створюючи простий модуль, який зробить все для вас, або ви можете піти правильним шляхом, є, композиція.

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

Зауважте, що назва змінної змінюється через семантику. Ви передаєте функтор (функцію, яку можна викликати іншою функцією) і отримуєте вказівник на пам'ять, який містить початковий блок логіки програми.

Але, якщо це не модуль, і вам потрібно експортувати логіку?

Оберніть функції у функцію асинхронізації.

export const readFilesQueue = async () => {
    // ... to code goes here
}

Або змінити назви змінних, що б там не було ...


* побічний ефект означає будь-який колатеральний ефект програми, який може змінити стан / поведінку або викликати помилки в програмі, як IO.

** "чистим", це в апострофі, оскільки функції не є чистими, і код можна конвертувати в чисту версію, коли немає консольного виводу, а лише маніпуляції з даними.

Крім цього, щоб бути чистим, вам потрібно буде працювати з монадами, які обробляють побічний ефект, схильний до помилок, і трактують цю помилку окремо від програми.


2

Одне важливе застереження : await + for .. ofМетод та forEach + asyncспосіб насправді мають різний ефект.

Приміщення awaitвсередині справжнього forциклу забезпечить виконання всіх асинхронних дзвінків по одному. І цей forEach + asyncспосіб одночасно зніме всі обіцянки, які швидше, але іноді перекриваються ( якщо ви робите якийсь запит БД або відвідуєте якісь веб-сервіси з обмеженням гучності і не хочете запускати 100 000 дзвінків одночасно).

Ви також можете використовувати reduce + promise(менш елегантно), якщо ви не використовуєте async/awaitі хочете переконатися, що файли читаються один за одним .

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

Або ви можете створити forEachAsync, щоб допомогти, але в основному використовувати те саме для циклу, що лежить в основі.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}

Подивіться, як визначити метод у javascript на Array.prototype та Object.prototype, щоб він не відображався в циклі . Також, ймовірно, ви повинні використовувати ту саму ітерацію, що і нативну forEach- доступ до індексів замість того, щоб покладатися на ітерабельність - і передавати індекс зворотному дзвінку.
Бергі

Ви можете використовувати Array.prototype.reduceспосіб, який використовує функцію асинхронізації. Я показав приклад у своїй відповіді: stackoverflow.com/a/49499491/2537258
Тимофій Зорн

2

За допомогою завдань, футуризації та переліку списків можна просто зробити

async function printFiles() {
  const files = await getFiles();

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

Ось як ви це налаштували

import fs from 'fs';
import { futurize } from 'futurize';
import Task from 'data.task';
import { List } from 'immutable-ext';

const future = futurizeP(Task)
const readFile = future(fs.readFile)

Ще один спосіб структурувати бажаний код був би

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

Або, можливо, навіть більш функціонально орієнтований

// 90% of encodings are utf-8, making that use case super easy is prudent

// handy-library.js
export const readFile = f =>
  future(fs.readFile)( f, 'utf-8' )

export const arrayToTaskList = list => taskFn => 
  List(files).traverse( Task.of, taskFn ) 

export const readFiles = files =>
  arrayToTaskList( files, readFile )

export const printFiles = files => 
  readFiles(files).fork( console.error, console.log)

Потім від батьківської функції

async function main() {
  /* awesome code with side-effects before */
  printFiles( await getFiles() );
  /* awesome code with side-effects after */
}

Якщо ви дійсно хотіли більшої гнучкості в кодуванні, ви можете просто зробити це (для задоволення, я використовую запропонований оператор Pipe Forward )

import { curry, flip } from 'ramda'

export const readFile = fs.readFile 
  |> future,
  |> curry,
  |> flip

export const readFileUtf8 = readFile('utf-8')

PS - Я не пробував цей код на консолі, можливо, були помилки друку ... "прямий фрістайл, зверху купола!" як би сказали діти 90-х. :-p


2

В даний час властивість прототипу Array.forEach не підтримує операції з асинхронізацією, але ми можемо створити власну полі-заповнення для задоволення наших потреб.

// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function 
async function asyncForEach(iteratorFunction){
  let indexer = 0
  for(let data of this){
    await iteratorFunction(data, indexer)
    indexer++
  }
}
// Append it as an Array prototype property
Array.prototype.asyncForEach = asyncForEach
module.exports = {Array}

І це все! Тепер у вас є метод async forEach, доступний для будь-яких масивів, визначених після них, до операцій.

Давайте перевіримо ...

// Nodejs style
// file: someOtherFile.js

const readline = require('readline')
Array = require('./asyncForEach').Array
const log = console.log

// Create a stream interface
function createReader(options={prompt: '>'}){
  return readline.createInterface({
    input: process.stdin
    ,output: process.stdout
    ,prompt: options.prompt !== undefined ? options.prompt : '>'
  })
}
// Create a cli stream reader
async function getUserIn(question, options={prompt:'>'}){
  log(question)
  let reader = createReader(options)
  return new Promise((res)=>{
    reader.on('line', (answer)=>{
      process.stdout.cursorTo(0, 0)
      process.stdout.clearScreenDown()
      reader.close()
      res(answer)
    })
  })
}

let questions = [
  `What's your name`
  ,`What's your favorite programming language`
  ,`What's your favorite async function`
]
let responses = {}

async function getResponses(){
// Notice we have to prepend await before calling the async Array function
// in order for it to function as expected
  await questions.asyncForEach(async function(question, index){
    let answer = await getUserIn(question)
    responses[question] = answer
  })
}

async function main(){
  await getResponses()
  log(responses)
}
main()
// Should prompt user for an answer to each question and then 
// log each question and answer as an object to the terminal

Ми могли б зробити те ж саме для деяких інших функцій масиву, як map ...

async function asyncMap(iteratorFunction){
  let newMap = []
  let indexer = 0
  for(let data of this){
    newMap[indexer] = await iteratorFunction(data, indexer, this)
    indexer++
  }
  return newMap
}

Array.prototype.asyncMap = asyncMap

... і так далі :)

Деякі речі, які слід зазначити:

  • Ваш iteratorFunction має бути функцією асинхронізації або обіцянкою
  • Будь-які масиви, створені раніше Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>, не матимуть цієї функції

2

Просто додавання до оригінальної відповіді

  • Синтаксис паралельного читання в оригінальній відповіді іноді заплутаний і важко читається, можливо, ми можемо записати його при іншому підході
async function printFiles() {
  const files = await getFilePaths();
  const fileReadPromises = [];

  const readAndLogFile = async filePath => {
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
    return contents;
  };

  files.forEach(file => {
    fileReadPromises.push(readAndLogFile(file));
  });

  await Promise.all(fileReadPromises);
}
  • Для послідовних операцій, а не лише для ... з , також буде працювати нормальне для циклу
async function printFiles() {
  const files = await getFilePaths();

  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
  }
}

1

Сьогодні я натрапив на це декілька рішень. Запуск функцій очікування асинхронізації в циклі forEach. Побудувавши обгортку навколо, ми можемо це зробити.

Більш детальне пояснення того, як це працює внутрішньо, для нативного ForEach і чому він не в змозі здійснити виклик функції асинхронізації та інші деталі щодо різних методів, наведені за посиланням тут

Кілька способів, через які це можна зробити, і вони є наступними,

Спосіб 1: Використання обгортки.

await (()=>{
     return new Promise((resolve,reject)=>{
       items.forEach(async (item,index)=>{
           try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
           count++;
           if(index === items.length-1){
             resolve('Done')
           }
         });
     });
    })();

Спосіб 2: Використання такої ж, як і загальна функція Array.prototype

Array.prototype.forEachAsync.js

if(!Array.prototype.forEachAsync) {
    Array.prototype.forEachAsync = function (fn){
      return new Promise((resolve,reject)=>{
        this.forEach(async(item,index,array)=>{
            await fn(item,index,array);
            if(index === array.length-1){
                resolve('done');
            }
        })
      });
    };
  }

Використання:

require('./Array.prototype.forEachAsync');

let count = 0;

let hello = async (items) => {

// Method 1 - Using the Array.prototype.forEach 

    await items.forEachAsync(async () => {
         try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
        count++;
    });

    console.log("count = " + count);
}

someAPICall = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("done") // or reject('error')
        }, 100);
    })
}

hello(['', '', '', '']); // hello([]) empty array is also be handled by default

Спосіб 3:

Використання Promise.all

  await Promise.all(items.map(async (item) => {
        await someAPICall();
        count++;
    }));

    console.log("count = " + count);

Спосіб 4: традиційний для циклу або сучасний для циклу

// Method 4 - using for loop directly

// 1. Using the modern for(.. in..) loop
   for(item in items){

        await someAPICall();
        count++;
    }

//2. Using the traditional for loop 

    for(let i=0;i<items.length;i++){

        await someAPICall();
        count++;
    }


    console.log("count = " + count);

Ваші методи 1 і 2 є просто неправильними реалізаціями, де їх Promise.allслід було використовувати - вони не враховують жодного з багатьох крайових випадків.
Берги

@Bergi: Дякую за вагомі коментарі, будь ласка, поясніть мені, чому метод 1 та 2 є невірними. Він також виконує мету. Це працює дуже добре. Це означає, що всі ці методи можливі, виходячи з ситуації, яку можна вирішити щодо вибору. У мене є приклад для того ж.
PranavKAndro

Він виходить з ладу на порожніх масивах, не має жодних помилок в обробці і, ймовірно, більше проблем. Не винаходити колесо. Просто використовуйте Promise.all.
Берги

У певних умовах, коли це неможливо, це буде корисно. Також обробка помилок здійснюється за замовчуванням forEach api, тому проблем не виникає. Про це подбали!
PranavKAndro

Ні, немає умов, де Promise.allце неможливо, але async/ awaitє. І ні, forEachабсолютно не справляється з будь-якими помилками обіцянки.
Берги

1

Це рішення також оптимізоване для пам’яті, тому ви можете запустити його на 10 000 елементів даних та запитів. Деякі інші рішення тут приведуть до збою сервера на великих наборах даних.

У TypeScript:

export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => void) {
        for (let index = 0; index < array.length; index++) {
            await callback(array[index], index);
        }
    }

Як користуватись?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})

1

Ви можете використовувати Array.prototype.forEach, але async / wait не настільки сумісний. Це пояснюється тим, що обіцянка, повернута з асинхронного зворотного дзвінка, очікує, що вона буде вирішена, але Array.prototype.forEachне вирішує жодних обіцянок від виконання її зворотного дзвінка. Отже, ви можете використовувати forEach, але вам доведеться самостійно впоратися з рішенням обіцянки.

Ось спосіб читати та друкувати кожен файл послідовно, використовуючи Array.prototype.forEach

async function printFilesInSeries () {
  const files = await getFilePaths()

  let promiseChain = Promise.resolve()
  files.forEach((file) => {
    promiseChain = promiseChain.then(() => {
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    })
  })
  await promiseChain
}

Ось спосіб (все ще використовується Array.prototype.forEach) паралельно друкувати вміст файлів

async function printFilesInParallel () {
  const files = await getFilePaths()

  const promises = []
  files.forEach((file) => {
    promises.push(
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    )
  })
  await Promise.all(promises)
}

Перший сенаріо ідеально підходить для циклів, які потрібно запустити в серії, і ви не можете їх використовувати
Марк Одей

0

Аналогічно Антоніо Валу p-iteration, альтернативним модулем npm є async-af:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  // since AsyncAF accepts promises or non-promises, there's no need to await here
  const files = getFilePaths();

  AsyncAF(files).forEach(async file => {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  });
}

printFiles();

Крім того, async-afмає статичний метод (log / logAF), який реєструє результати обіцянок:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  const files = getFilePaths();

  AsyncAF(files).forEach(file => {
    AsyncAF.log(fs.readFile(file, 'utf8'));
  });
}

printFiles();

Однак головна перевага бібліотеки полягає в тому, що ви можете зв'язати асинхронні методи, щоб зробити щось на кшталт:

const aaf = require('async-af');
const fs = require('fs-promise');

const printFiles = () => aaf(getFilePaths())
  .map(file => fs.readFile(file, 'utf8'))
  .forEach(file => aaf.log(file));

printFiles();

async-af


0

Щоб побачити, як це може піти не так, надрукуйте console.log в кінці методу.

Те, що в цілому може піти не так:

  • Довільний порядок.
  • Файли друку можуть закінчити роботу перед друком файлів.
  • Погана продуктивність.

Вони не завжди помиляються, але часто трапляються у звичайних випадках використання.

Як правило, використання forEach призведе до всього, але останнього. Він зателефонує до кожної функції, не чекаючи, коли функція означає, що вона повідомляє, що всі функції можна запустити, а потім закінчити, не чекаючи завершення функцій.

import fs from 'fs-promise'

async function printFiles () {
  const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'))

  for(const file of files)
    console.log(await file)
}

printFiles()

Це приклад у рідному JS, який дозволить зберегти порядок, запобігти передчасному поверненню функції і теоретично збереже оптимальні показники.

Це буде:

  • Ініціюйте, щоб усі читання файлів відбувалися паралельно.
  • Збережіть порядок, використовуючи карту для картографування імен файлів, на які обіцяє чекати.
  • Зачекайте кожної обіцянки в порядку, визначеному масивом.

З цим рішенням перший файл буде показаний, як тільки він буде доступний, не чекаючи, коли інші стануть доступними першими.

Це також буде завантажувати всі файли одночасно, а не потрібно чекати, коли перший закінчиться, перш ніж можна буде розпочати зчитування другого файлу.

Єдиним недоліком цієї та оригінальної версії є те, що якщо запускати кілька читання відразу, то важче обробляти помилки за рахунок появи більше помилок, які можуть статися за один раз.

З версіями, які читають файл за один раз, потім вони зупиняються на збої, не витрачаючи часу на спроби прочитати більше файлів. Навіть із складною системою скасування може бути важко уникнути її відмови в першому файлі, але також читає більшість інших файлів.

Продуктивність не завжди передбачувана. Хоча багато систем будуть швидшими при паралельному зчитуванні файлів, деякі віддадуть перевагу послідовному. Деякі з них є динамічними і можуть змінюватися під навантаженням, оптимізації, які пропонують затримку, не завжди дають хорошу пропускну здатність в умовах великої суперечки.

У цьому прикладі також немає помилок. Якщо щось вимагає, щоб вони або були успішно показані, або взагалі не зробили це.

Рекомендується поглиблене експериментувати з console.log на кожному етапі та підробляти рішення для читання файлів (замість них випадкова затримка). Хоча, як видається, багато рішень роблять те ж саме у простих випадках, у всіх є тонкі відмінності, які потребують додаткової перевірки, щоб вичавити.

Використовуйте цей макет, щоб допомогти визначити різницю між рішеннями:

(async () => {
  const start = +new Date();
  const mock = () => {
    return {
      fs: {readFile: file => new Promise((resolve, reject) => {
        // Instead of this just make three files and try each timing arrangement.
        // IE, all same, [100, 200, 300], [300, 200, 100], [100, 300, 200], etc.
        const time = Math.round(100 + Math.random() * 4900);
        console.log(`Read of ${file} started at ${new Date() - start} and will take ${time}ms.`)
        setTimeout(() => {
          // Bonus material here if random reject instead.
          console.log(`Read of ${file} finished, resolving promise at ${new Date() - start}.`);
          resolve(file);
        }, time);
      })},
      console: {log: file => console.log(`Console Log of ${file} finished at ${new Date() - start}.`)},
      getFilePaths: () => ['A', 'B', 'C', 'D', 'E']
    };
  };

  const printFiles = (({fs, console, getFilePaths}) => {
    return async function() {
      const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'));

      for(const file of files)
        console.log(await file);
    };
  })(mock());

  console.log(`Running at ${new Date() - start}`);
  await printFiles();
  console.log(`Finished running at ${new Date() - start}`);
})();

-3

Я б використовував добре перевірені (мільйони завантажень на тиждень) модулі pify та async . Якщо ви не знайомі з модулем асинхронізації, настійно рекомендую ознайомитись з його документами . Я бачив, як багато розробників витрачають час на відтворення своїх методів, або ще гірше, що ускладнює підтримку асинхронного коду, коли методи асинхронного вищого порядку спрощують код.

const async = require('async')
const fs = require('fs-promise')
const pify = require('pify')

async function getFilePaths() {
    return Promise.resolve([
        './package.json',
        './package-lock.json',
    ]);
}

async function printFiles () {
  const files = await getFilePaths()

  await pify(async.eachSeries)(files, async (file) => {  // <-- run in series
  // await pify(async.each)(files, async (file) => {  // <-- run in parallel
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
  console.log('HAMBONE')
}

printFiles().then(() => {
    console.log('HAMBUNNY')
})
// ORDER OF LOGS:
// package.json contents
// package-lock.json contents
// HAMBONE
// HAMBUNNY
```


Це крок у неправильному напрямку. Ось посібник із картографування, який я створив, щоб допомогти людям застрягнути в пеклі зворотного виклику в сучасну епоху JS: github.com/jmjpro/async-package-to-async-await/blob/master/… .
jbustamovej

як ви бачите тут , я зацікавлений і відкритий для використання функції async / await замість асинхронної вставки. Зараз я думаю, що в кожного є свій час і місце. Я не переконаний, що async lib == "пекло зворотного виклику" та async / wait == "сучасна ера JS". imo, коли async lib> async / очікує: 1. складний потік (наприклад, черга, вантаж, навіть авто, коли речі ускладнюються) 2. одночасність 3. підтримка масивів / об’єктів / ітерабелів 4. поводження з помилками
Zachary Ryan Smith
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.