Menggunakan async / tunggu dengan forEach loop

1132

Apakah ada masalah dengan menggunakan async/ awaitdalam satu forEachlingkaran? Saya mencoba untuk mengulang melalui array file dan awaitpada isi setiap file.

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()

Kode ini berfungsi, tetapi bisakah ada yang salah dengan ini? Saya memiliki seseorang yang memberi tahu saya bahwa Anda tidak seharusnya menggunakan async/ awaitdalam fungsi tingkat tinggi seperti ini, jadi saya hanya ingin bertanya apakah ada masalah dengan ini.

saadq
sumber

Jawaban:

2156

Tentu kode itu berfungsi, tapi saya cukup yakin itu tidak melakukan apa yang Anda harapkan. Itu hanya memadamkan beberapa panggilan tidak sinkron, tetapi printFilesfungsi tidak segera kembali setelah itu.

Membaca secara berurutan

Jika Anda ingin membaca file secara berurutan, AndaforEach memang tidak dapat menggunakannya . Cukup gunakan for … ofloop modern sebagai gantinya, yang awaitakan bekerja seperti yang diharapkan:

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

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

Membaca secara paralel

Jika Anda ingin membaca file secara paralel, AndaforEach memang tidak bisa menggunakannya . Setiap asyncpanggilan fungsi panggilan balik tidak memberikan janji, tetapi Anda membuangnya alih-alih menunggu. Cukup gunakan mapsaja, dan Anda dapat menunggu berbagai janji yang akan Anda dapatkan dengan 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)
  }));
}
Bergi
sumber
33
Bisakah Anda jelaskan mengapa itu for ... of ...berhasil?
Demonbane
84
ok saya tahu mengapa ... Menggunakan Babel akan mengubah async/ awaitke fungsi generator dan menggunakan forEachberarti bahwa setiap iterasi memiliki fungsi generator individu, yang tidak ada hubungannya dengan yang lain. sehingga mereka akan dieksekusi secara independen dan tidak memiliki konteks next()dengan orang lain. Sebenarnya, for()loop sederhana juga berfungsi karena iterasi juga dalam satu fungsi generator tunggal.
Demonbane
21
@Demonbane: Singkatnya, karena dirancang untuk bekerja :-) awaitmenunda evaluasi fungsi saat ini , termasuk semua struktur kontrol. Ya, ini sangat mirip dengan generator dalam hal itu (itulah sebabnya mereka digunakan untuk polyfill async / menunggu).
Bergi
3
@ arve0 Tidak juga, suatu asyncfungsi sangat berbeda dari Promisepanggilan balik pelaksana, tapi ya mappanggilan balik mengembalikan janji dalam kedua kasus.
Bergi
5
Ketika Anda datang untuk belajar tentang janji-janji JS, tetapi alih-alih gunakan setengah jam menerjemahkan latin. Semoga Anda bangga @Bergi;)
Félix Gagnon-Grenier
190

Dengan ES2018, Anda dapat sangat menyederhanakan semua jawaban di atas untuk:

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

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

Lihat spec: proposal-async-iteration


2018-09-10: Jawaban ini telah mendapat banyak perhatian baru-baru ini, silakan lihat posting blog Axel Rauschmayer untuk informasi lebih lanjut tentang iterasi asinkron: ES2018: iterasi asinkron

Francisco Mateo
sumber
4
Terpilih, akan lebih bagus jika Anda bisa menempatkan tautan ke spec dalam jawaban Anda untuk siapa saja yang ingin tahu lebih banyak tentang iterasi asinkron.
saadq
8
Bukankah seharusnya konten bukan file di iterator
FluffyBeing
10
Mengapa orang memilih jawaban ini? Perhatikan lebih dekat jawaban, pertanyaan, dan proposal. Setelah itu ofharus fungsi async yang akan mengembalikan array. Itu tidak berhasil dan Francisco berkata;
Yevhenii Herasymchuk
3
Sepenuhnya setuju dengan @AntonioVal. Itu bukan jawaban.
Yevhenii Herasymchuk
2
Meskipun saya setuju itu bukan jawaban, membatalkan proposal adalah cara untuk meningkatkan popularitasnya sehingga membuatnya tersedia lebih awal untuk digunakan nanti.
Robert Molina
62

Alih-alih Promise.allbersamaan Array.prototype.map(yang tidak menjamin urutan Promisepenyelesaian s), saya menggunakan Array.prototype.reduce, dimulai dengan yang diselesaikan 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());
}
Timothy Zorn
sumber
1
Ini berfungsi dengan baik, terima kasih banyak. Bisakah Anda menjelaskan apa yang terjadi di sini dengan Promise.resolve()dan await promise;?
parrker9
1
Ini keren sekali. Apakah saya benar dalam berpikir file akan dibaca secara berurutan dan tidak sekaligus?
GollyJer
1
@ parrker9 Promise.resolve()mengembalikan yang sudah diselesaikan Promiseobjek, sehingga reducememiliki Promiseuntuk memulai dengan. await promise;akan menunggu yang terakhir Promisedalam rantai untuk diselesaikan. @GollyJer File akan diproses secara berurutan, satu per satu.
Timothy Zorn
Penggunaan pengurangan yang sangat keren, terima kasih atas komentarnya! Saya hanya akan menunjukkan bahwa, berbeda dengan beberapa metode lain yang disebutkan dalam komentar, yang satu ini sinkron, artinya file-file tersebut dibaca satu demi satu dan tidak paralel (karena iterasi berikutnya dari fungsi pengurangan bergantung pada sebelumnya iterasi, harus sinkron).
Shay Yzhakov
1
@Shay, maksudmu berurutan, tidak sinkron. Ini masih asinkron - jika hal-hal lain dijadwalkan, mereka akan berjalan di antara iterasi di sini.
Timothy Zorn
33

The p-iterasi modul alat NPM metode Array iterasi sehingga mereka dapat digunakan dalam cara yang sangat sederhana dengan async / Tunggulah.

Contoh dengan kasing Anda:

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);
  });
})();
Antonio Val
sumber
1
Saya suka ini karena memiliki fungsi / metode yang sama dengan JS itu sendiri - dalam kasus saya, saya perlu somedaripada forEach. Terima kasih!
mikemaccana
26

Berikut ini beberapa forEachAsyncprototipe. Perhatikan bahwa Anda akan membutuhkannya 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));
}

Catatan sementara Anda dapat memasukkan ini dalam kode Anda sendiri, Anda tidak harus memasukkan ini di perpustakaan yang Anda distribusikan kepada orang lain (untuk menghindari mencemari global mereka).

Mat
sumber
1
Meskipun saya ragu untuk menambahkan hal-hal langsung ke prototipe, ini async bagus untuk implementasi Setiap
DaniOcean
2
Selama namanya unik di masa depan (seperti yang akan saya gunakan _forEachAsync) ini masuk akal. Saya juga berpikir itu adalah jawaban terbaik karena menghemat banyak kode boilerplate.
mikemaccana
1
@estus Yaitu untuk menghindari mencemari kode orang lain. Jika kode itu milik organisasi pribadi kita, dan global ada dalam file yang teridentifikasi dengan baik ( globals.jsakan baik) kita dapat menambahkan global yang kita inginkan.
mikemaccana
1
@mikemaccana Itu untuk menghindari praktik buruk yang diterima secara umum. Itu benar, ini bisa dilakukan selama Anda hanya menggunakan kode pihak pertama, yang jarang terjadi. Masalahnya adalah ketika Anda menggunakan lib pihak ketiga, mungkin ada beberapa lelaki lain yang merasakan hal yang sama dan memodifikasi global yang sama, hanya karena itu sepertinya ide yang baik pada saat lib ditulis.
Estus Flask
1
@estus Tentu. Saya telah menambahkan peringatan pada pertanyaan untuk menyimpan diskusi (tidak terlalu produktif) di sini.
mikemaccana
7

Selain jawaban @ Bergi , saya ingin menawarkan alternatif ketiga. Ini sangat mirip dengan contoh ke-2 @ Bergi, tetapi alih-alih menunggu masing readFile- masing secara individu, Anda membuat serangkaian janji, yang masing-masing Anda tunggu di bagian akhir.

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);
}

Perhatikan bahwa fungsi yang diteruskan .map()tidak perlu async, karena fs.readFilemengembalikan objek Janji saja. Oleh karena itu promisesadalah array objek Janji, yang dapat dikirim ke Promise.all().

Dalam jawaban @ Bergi, konsol dapat mencatat konten file sesuai urutan pembacaannya. Sebagai contoh jika file sangat kecil selesai membaca sebelum file sangat besar, itu akan dicatat terlebih dahulu, bahkan jika file kecil datang setelah file besar dalam filesarray. Namun, dalam metode saya di atas, Anda dijamin konsol akan mencatat file dalam urutan yang sama dengan array yang disediakan.

chharvey
sumber
1
Saya yakin Anda salah: Saya cukup yakin metode Anda juga dapat membaca file yang rusak. Ya, itu akan mencatat output dalam urutan yang benar (karena itu await Promise.all), tetapi file mungkin telah dibaca dalam urutan yang berbeda, yang bertentangan dengan pernyataan Anda "Anda dijamin konsol akan mencatat file dalam urutan yang sama seperti mereka Baca".
Venryx
1
@Venryx Anda benar, terima kasih atas koreksinya. Saya telah memperbarui jawaban saya.
chharvey
6

Solusi Bergi bekerja dengan baik ketika fsdidasarkan pada janji. Anda dapat menggunakan bluebird, fs-extraatau fs-promiseuntuk ini.

Namun, solusi untukfs perpustakaan asli simpul adalah sebagai berikut:

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;
    }));

Catatan: require('fs') wajib berfungsi sebagai argumen ke-3, jika tidak menimbulkan kesalahan:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
myDoggyWritesCode
sumber
5

Kedua solusi di atas bekerja, namun, Antonio melakukan pekerjaan dengan kode yang lebih sedikit, di sini adalah bagaimana ini membantu saya menyelesaikan data dari database saya, dari beberapa referensi anak yang berbeda dan kemudian mendorong mereka semua ke dalam array dan menyelesaikannya dalam janji setelah semua selesai:

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)))
Hooman Askari
sumber
4

cukup menyakitkan untuk memunculkan beberapa metode dalam file yang akan menangani data asinkron dalam urutan serial dan memberikan rasa yang lebih konvensional pada kode Anda. Sebagai contoh:

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;
  };
};

sekarang, dengan asumsi itu disimpan di './myAsync.js' Anda dapat melakukan sesuatu yang mirip dengan di bawah ini dalam file yang berdekatan:

...
/* 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));
}
Jay Edwards
sumber
2
Tambahan kecil, jangan lupa untuk membungkus menunggu / asinks Anda dalam blok coba / tangkap !!
Jay Edwards
4

Suka @ Bergi, tetapi dengan satu perbedaan.

Promise.all menolak semua janji jika ada yang ditolak.

Jadi, gunakan rekursi.

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

readFilesQueueberada di luar printFilessebab efek samping * yang diperkenalkan olehconsole.log , lebih baik mengejek, menguji, atau memata-matai, tidak keren memiliki fungsi yang mengembalikan konten (sidenote).

Oleh karena itu, kode hanya dapat dirancang dengan itu: tiga fungsi terpisah yang "murni" ** dan tidak menimbulkan efek samping, memproses seluruh daftar dan dapat dengan mudah dimodifikasi untuk menangani kasus yang gagal.

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)

Sunting / kondisi saat ini di masa depan

Node mendukung top-level menanti (ini belum memiliki plugin, tidak akan memiliki dan dapat diaktifkan melalui bendera harmoni), itu keren tetapi tidak menyelesaikan satu masalah (secara strategis saya hanya bekerja pada versi LTS). Bagaimana cara mendapatkan file?

Menggunakan komposisi. Mengingat kode itu, membuat saya merasa bahwa ini ada di dalam modul, jadi, harus memiliki fungsi untuk melakukannya. Jika tidak, Anda harus menggunakan IIFE untuk membungkus kode peran ke dalam fungsi async membuat modul sederhana yang melakukan semuanya untuk Anda, atau Anda dapat pergi dengan cara yang benar, yaitu, komposisi.

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

Perhatikan bahwa nama variabel berubah karena semantik. Anda melewatkan functor (fungsi yang dapat dipanggil oleh fungsi lain) dan menerima pointer pada memori yang berisi blok awal logika aplikasi.

Tetapi, jika ini bukan modul dan Anda perlu mengekspor logikanya?

Bungkus fungsi dalam fungsi async.

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

Atau ubah nama variabel, apa pun ...


* by side effect menans efek kolakteral dari aplikasi yang dapat mengubah status / perilaku atau memperkenalkan bug dalam aplikasi, seperti IO.

** oleh "pure", itu dalam tanda kutip karena fungsi itu tidak murni dan kode dapat dikonversikan ke versi murni, ketika tidak ada output konsol, hanya manipulasi data.

Selain itu, untuk menjadi murni, Anda harus bekerja dengan monad yang menangani efek samping, yang rentan kesalahan, dan memperlakukan kesalahan itu secara terpisah dari aplikasi.

lukaswilkeer
sumber
3

Satu peringatan penting adalah: await + for .. ofMetode danforEach + async caranya sebenarnya memiliki efek yang berbeda.

Setelah awaitdi dalam forloop nyata akan memastikan semua panggilan async dieksekusi satu per satu. Dan forEach + asynccaranya akan memadamkan semua janji pada saat yang sama, yang lebih cepat tetapi terkadang kewalahan ( jika Anda melakukan permintaan DB atau mengunjungi beberapa layanan web dengan batasan volume dan tidak ingin memecat 100.000 panggilan sekaligus).

Anda juga bisa menggunakan reduce + promise (kurang elegan) jika Anda tidak menggunakan async/awaitdan ingin memastikan file dibaca satu demi satu .

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

Atau Anda dapat membuat forEachAsync untuk membantu tetapi pada dasarnya menggunakan hal yang sama untuk loop yang mendasarinya.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
LeOn - Han Li
sumber
Lihat bagaimana cara mendefinisikan metode dalam javascript di Array.prototype dan Object.prototype sehingga tidak muncul dalam untuk di dalam loop . Anda juga mungkin harus menggunakan iterasi yang sama dengan asli forEach- mengakses indeks alih-alih mengandalkan iterability - dan meneruskan indeks ke callback.
Bergi
Anda dapat menggunakan Array.prototype.reducecara yang menggunakan fungsi async. Saya telah menunjukkan contoh dalam jawaban saya: stackoverflow.com/a/49499491/2537258
Timothy Zorn
3

Dengan menggunakan Task, futurize, dan Daftar yang dapat dilalui, Anda dapat melakukannya

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

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

Inilah cara Anda mengaturnya

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)

Cara lain untuk menyusun kode yang diinginkan adalah

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

Atau mungkin lebih berorientasi fungsional

// 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)

Kemudian dari fungsi induk

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

Jika Anda benar-benar menginginkan lebih banyak fleksibilitas dalam penyandian, Anda bisa melakukan ini (untuk bersenang-senang, saya menggunakan operator Pipe Forward yang diusulkan )

import { curry, flip } from 'ramda'

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

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

PS - Saya tidak mencoba kode ini di konsol, mungkin memiliki beberapa kesalahan ketik ... "gaya bebas lurus, dari atas kubah!" seperti yang dikatakan anak-anak tahun 90-an. :-p

Babakness
sumber
3

Saat ini properti prototipe Array.forEach tidak mendukung operasi async, tetapi kami dapat membuat poly-fill kami sendiri untuk memenuhi kebutuhan kami.

// 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}

Dan itu dia! Anda sekarang memiliki metode async forEach yang tersedia pada array apa pun yang didefinisikan setelah ini untuk operasi.

Ayo kita coba ...

// 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

Kita dapat melakukan hal yang sama untuk beberapa fungsi array lainnya seperti peta ...

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

... dan seterusnya :)

Beberapa hal yang perlu diperhatikan:

  • Fungsi iterator Anda harus berupa fungsi atau janji async
  • Setiap array yang dibuat sebelumnya Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>tidak akan memiliki fitur ini
Pesolek
sumber
3

Hanya menambah jawaban asli

  • Sintaks membaca paralel dalam jawaban asli terkadang membingungkan dan sulit dibaca, mungkin kita bisa menulisnya dengan pendekatan yang berbeda
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);
}
  • Untuk operasi berurutan, bukan hanya untuk ... dari , normal untuk loop juga akan berfungsi
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);
  }
}
gsaandy
sumber
2

Untuk melihat bagaimana itu bisa salah, cetak console.log di akhir metode.

Hal-hal yang bisa salah secara umum:

  • Pesanan sewenang-wenang.
  • printFiles dapat selesai berjalan sebelum mencetak file.
  • Penampilan buruk.

Ini tidak selalu salah tetapi sering dalam kasus penggunaan standar.

Secara umum, menggunakan forEach akan menghasilkan semua kecuali yang terakhir. Ini akan memanggil setiap fungsi tanpa menunggu fungsi yang berarti memberitahu semua fungsi untuk memulai kemudian selesai tanpa menunggu fungsi selesai.

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()

Ini adalah contoh dalam JS asli yang akan menjaga ketertiban, mencegah fungsi kembali sebelum waktunya dan secara teori mempertahankan kinerja yang optimal.

Ini akan:

  • Memulai semua file yang dibaca terjadi secara paralel.
  • Pertahankan pesanan melalui penggunaan peta untuk memetakan nama file agar dijanjikan akan menunggu.
  • Tunggu setiap janji dalam urutan yang ditentukan oleh array.

Dengan solusi ini, file pertama akan ditampilkan segera setelah tersedia tanpa harus menunggu yang lain tersedia terlebih dahulu.

Ini juga akan memuat semua file pada saat yang sama daripada harus menunggu yang pertama selesai sebelum membaca file kedua dapat dimulai.

Satu-satunya kelemahan dari ini dan versi aslinya adalah bahwa jika banyak pembacaan dimulai sekaligus maka lebih sulit untuk menangani kesalahan karena memiliki lebih banyak kesalahan yang dapat terjadi pada suatu waktu.

Dengan versi yang membaca file pada suatu waktu maka akan berhenti pada kegagalan tanpa membuang waktu mencoba membaca file lagi. Bahkan dengan sistem pembatalan yang rumit, bisa sulit untuk menghindarinya gagal pada file pertama tetapi sudah membaca sebagian besar file lainnya juga.

Kinerja tidak selalu dapat diprediksi. Sementara banyak sistem akan lebih cepat dengan membaca file paralel beberapa akan lebih suka berurutan. Beberapa dinamis dan mungkin bergeser di bawah beban, optimisasi yang menawarkan latensi tidak selalu menghasilkan throughput yang baik di bawah pertengkaran berat.

Tidak ada penanganan kesalahan dalam contoh itu. Jika sesuatu mengharuskan mereka untuk berhasil ditampilkan atau tidak, itu tidak akan berhasil.

Eksperimen mendalam dianjurkan dengan console.log pada setiap tahap dan solusi pembacaan file palsu (sebagai gantinya penundaan). Meskipun banyak solusi tampaknya melakukan hal yang sama dalam kasus-kasus sederhana semua memiliki perbedaan halus yang memerlukan beberapa pemeriksaan ekstra untuk memeras.

Gunakan tiruan ini untuk membantu membedakan antara solusi:

(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}`);
})();
jgmjgm
sumber
2

Hari ini saya menemukan beberapa solusi untuk ini. Menjalankan async menunggu fungsi di forEach Loop. Dengan membangun pembungkus di sekitar kita dapat mewujudkannya.

Penjelasan lebih rinci tentang cara kerjanya secara internal, untuk forach asli dan mengapa tidak dapat membuat panggilan fungsi async dan rincian lainnya tentang berbagai metode disediakan di tautan di sini

Berbagai cara yang melaluinya dapat dilakukan dan mereka adalah sebagai berikut,

Metode 1: Menggunakan pembungkus.

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')
           }
         });
     });
    })();

Metode 2: Menggunakan yang sama sebagai fungsi generik 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');
            }
        })
      });
    };
  }

Penggunaan:

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

Metode 3:

Menggunakan Promise.all

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

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

Metode 4: Tradisional untuk loop atau modern untuk loop

// 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);
PranavKAndro
sumber
Metode Anda 1 dan 2 hanyalah implementasi yang salah di mana Promise.allseharusnya digunakan - mereka tidak memperhitungkan banyak kasus tepi.
Bergi
@Bergi: Terima kasih atas komentar yang valid, Tolong jelaskan mengapa metode 1 dan 2 salah. Itu juga melayani tujuan. Ini bekerja dengan sangat baik. Ini untuk mengatakan bahwa semua metode ini dimungkinkan, berdasarkan situasi yang dapat diputuskan seseorang untuk memilih satu. Saya punya contoh untuk hal yang sama.
PranavKAndro
Gagal pada array kosong, tidak memiliki penanganan kesalahan, dan mungkin lebih banyak masalah. Jangan menemukan kembali roda. Gunakan saja Promise.all.
Bergi
Dalam kondisi tertentu di mana itu tidak mungkin akan sangat membantu. Juga penanganan kesalahan dilakukan oleh forEach api secara default sehingga tidak ada masalah. Itu diurus!
PranavKAndro
Tidak, tidak ada kondisi di mana Promise.alltidak mungkin tetapi async/ awaitadalah. Dan tidak, forEachsama sekali tidak menangani kesalahan janji.
Bergi
2

Solusi ini juga dioptimalkan untuk memori sehingga Anda dapat menjalankannya pada 10.000 item data dan permintaan. Beberapa solusi lain di sini akan membuat server crash pada kumpulan data besar.

Dalam 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);
        }
    }

Cara Penggunaan?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
Oliver Dixon
sumber
2

Anda dapat menggunakan Array.prototype.forEach, tetapi async / menunggu tidak begitu kompatibel. Ini karena janji yang dikembalikan dari panggilan balik async mengharapkan untuk diselesaikan, tetapi Array.prototype.forEachtidak menyelesaikan janji apa pun dari pelaksanaan panggilan baliknya. Jadi, Anda bisa menggunakan forEach, tetapi Anda harus menangani sendiri resolusi janji.

Berikut adalah cara untuk membaca dan mencetak setiap file secara seri menggunakan 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
}

Berikut adalah cara (masih menggunakan Array.prototype.forEach) untuk mencetak konten file secara paralel

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)
}
richytong
sumber
Senario pertama sangat ideal untuk loop yang perlu dijalankan dalam seri dan Anda tidak dapat menggunakannya untuk
Mark Odey
0

Mirip dengan Antonio Val's p-iteration, modul npm alternatif adalah 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();

Atau, async-afmemiliki metode statis (log / logAF) yang mencatat hasil janji:

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();

Namun, keuntungan utama perpustakaan adalah Anda dapat membuat metode asinkron untuk melakukan sesuatu seperti:

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

Scott Rudiger
sumber
-3

Saya akan menggunakan modul pify dan async (jutaan unduhan per minggu) yang teruji dengan baik . Jika Anda tidak terbiasa dengan modul async, saya sangat menyarankan Anda memeriksa dokumennya . Saya telah melihat banyak devs membuang-buang waktu untuk membuat ulang metodenya, atau lebih buruk lagi, membuat kode async sulit dipelihara ketika metode async tingkat tinggi akan menyederhanakan kode.

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
```

Zachary Ryan Smith
sumber
Ini adalah langkah ke arah yang salah. Berikut adalah panduan pemetaan yang saya buat untuk membantu orang-orang terjebak dalam neraka panggilan balik ke era JS modern: github.com/jmjpro/async-package-to-async-await/blob/master/… .
jbustamovej
seperti yang Anda lihat di sini , saya tertarik dan terbuka untuk menggunakan async / menunggu daripada async lib. Saat ini, saya pikir masing-masing memiliki waktu dan tempat. Saya tidak yakin bahwa async lib == "callback hell" dan async / await == "the JS era modern". imo, ketika async lib> async / menunggu: 1. aliran kompleks (mis., antrian, kargo, bahkan otomatis ketika segala sesuatunya menjadi rumit) 2. konkurensi 3. array / benda / barang yang mendukung 4. penanganan yang salah
Zachary Ryan Smith