Використання 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.