ใช้ async / await กับ forEach loop


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ในลำดับการทำงานที่สูงขึ้นเช่นนี้ดังนั้นฉันแค่อยากจะถามว่ามันมีปัญหาหรือไม่

คำตอบ:


2145

แน่ใจว่ารหัสใช้งานได้ แต่ฉันค่อนข้างแน่ใจว่ามันไม่ได้ทำตามที่คุณคาดหวัง มันเพิ่งจะปิดการเรียกแบบอะซิงโครนัสหลายสาย แต่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ระงับการประเมินฟังก์ชั่นปัจจุบันรวมถึงโครงสร้างการควบคุมทั้งหมด ใช่มันค่อนข้างคล้ายกับเครื่องกำเนิดไฟฟ้าในเรื่องนั้น (ซึ่งเป็นเหตุผลที่พวกเขาคุ้นเคยกับ polyfill async / คอย)
Bergi

3
@ arve0 ไม่จริงasyncฟังก์ชั่นจะค่อนข้างแตกต่างจากการPromiseโทรกลับของผู้บริหาร แต่ใช่mapโทรกลับส่งกลับสัญญาในทั้งสองกรณี
Bergi

5
เมื่อคุณมาเรียนรู้เกี่ยวกับสัญญาของ JS แต่ใช้การแปลละตินเป็นเวลาครึ่งชั่วโมงแทน หวังว่าคุณจะภูมิใจ @Bergi;)
Félix Gagnon-Grenier

188

ด้วย ES2018 คุณสามารถลดความซับซ้อนของคำตอบทั้งหมดข้างต้นให้เป็น:

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

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

ดูข้อมูลจำเพาะ: ข้อเสนอ-async-iteration


2018-09-10: คำตอบนี้ได้รับความสนใจอย่างมากเมื่อเร็ว ๆ นี้โปรดดูโพสต์บล็อกของ Axel Rauschmayer สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการทำซ้ำแบบอะซิงโครนัส: ES2018: การทำซ้ำแบบอะซิงโครนัส


4
การโหวตจะดีมากถ้าคุณสามารถใส่ลิงค์ไปยังข้อมูลจำเพาะในคำตอบของคุณสำหรับผู้ที่ต้องการทราบเพิ่มเติมเกี่ยวกับการทำซ้ำ async
saadq

8
ไม่ควรเป็นเนื้อหาแทนที่จะเป็นไฟล์ในตัววนซ้ำ
FluffyBeing

10
ทำไมผู้คนถึงไม่ยอมตอบคำถามนี้? ดูคำตอบคำถามและข้อเสนอที่ละเอียดยิ่งขึ้น หลังจากนั้นofควรเป็นฟังก์ชั่น async ซึ่งจะส่งกลับอาร์เรย์ มันใช้งานไม่ได้และฟรานซิสโกพูด
Yevhenii Herasymchuk

3
เห็นด้วยอย่างยิ่งกับ @AntonioVal มันไม่ใช่คำตอบ
Yevhenii Herasymchuk

2
แม้ว่าฉันจะเห็นด้วยไม่ใช่คำตอบการถอนข้อเสนอเป็นวิธีหนึ่งที่จะเพิ่มความนิยมให้กับผู้ใช้ก่อนหน้านี้เพื่อใช้ในภายหลัง
Robert Molina

61

แทนที่จะPromise.allใช้ร่วมกับArray.prototype.map(ซึ่งไม่รับประกันการสั่งซื้อที่Promiseแก้ไขได้) ฉันใช้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 ไฟล์จะถูกประมวลผลตามลำดับทีละไฟล์
Timothy Zorn

ใช้ลดความเท่ห์มากขอบคุณสำหรับความคิดเห็น! ฉันจะแสดงให้เห็นว่าในทางตรงกันข้ามกับวิธีอื่น ๆ ที่กล่าวถึงในความคิดเห็นวิธีนี้เป็นแบบซิงโครนัสซึ่งหมายความว่าไฟล์จะถูกอ่านทีละไฟล์และไม่ขนานกัน (เนื่องจากการทำซ้ำของฟังก์ชั่นการลดถัดไป การทำซ้ำจะต้องซิงโครนัส)
Shay Yzhakov

1
@Shay คุณหมายถึงการเรียงลำดับไม่ใช่แบบซิงโครนัส สิ่งนี้ยังคงไม่ตรงกันหากมีการกำหนดเวลาอย่างอื่นสิ่งเหล่านี้จะทำงานระหว่างการทำซ้ำที่นี่
Timothy Zorn

32

P-ซ้ำโมดูลในการดำเนินการ NPM วิธีอาร์เรย์ซ้ำเพื่อให้พวกเขาสามารถนำมาใช้ในทางที่ตรงไปตรงมามากกับ async / รอคอย

ตัวอย่างกับกรณีของคุณ:

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
แม้ว่าฉันจะลังเลที่จะเพิ่มสิ่งต่าง ๆ ลงในต้นแบบโดยตรง แต่นี่เป็น async ที่ดีสำหรับการใช้งานแต่ละครั้ง
DaniOcean

2
ตราบใดที่ชื่อนั้นไม่เหมือนใครในอนาคต (เหมือนที่ฉันใช้_forEachAsync) นี่ก็สมเหตุสมผล ฉันยังคิดว่ามันเป็นคำตอบที่ดีที่สุดเพราะมันจะบันทึกรหัสสำเร็จรูปจำนวนมาก
mikemaccana

1
@estus นั่นคือการหลีกเลี่ยงการสร้างรหัสของคนอื่น หากรหัสเป็นขององค์กรส่วนบุคคลของเราและ globals อยู่ในไฟล์ที่ระบุได้ดี ( globals.jsน่าจะดี) เราสามารถเพิ่ม globals ได้ตามที่เราต้องการ
mikemaccana

1
@mikemaccana เพื่อหลีกเลี่ยงการปฏิบัติที่ไม่ดี จริงอยู่นี้สามารถทำได้ตราบใดที่คุณใช้เฉพาะรหัสบุคคลที่หนึ่งซึ่งเกิดขึ้นน้อยมาก ปัญหาคือเมื่อคุณใช้ libs ของบุคคลที่สามอาจมีผู้ชายบางคนที่รู้สึกแบบเดียวกันและปรับเปลี่ยนกลมเดียวกันเพียงเพราะมันดูเหมือนเป็นความคิดที่ดีในเวลาที่เขียน lib
Estus Flask

1
@ แน่นอนว่า ฉันได้เพิ่มคำเตือนไปยังคำถามเพื่อบันทึกการอภิปราย
mikemaccana

6

นอกจากคำตอบของ @ Bergiฉันต้องการเสนอทางเลือกที่สาม มันคล้ายกับตัวอย่างที่ 2 ของ @ Bergi มาก แต่แทนที่จะรอแต่ละ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ส่งกลับวัตถุสัญญา ดังนั้นจึงเป็นอาร์เรย์ของวัตถุสัญญาซึ่งสามารถส่งไปยังpromisesPromise.all()

ในคำตอบของ @ Bergi คอนโซลอาจบันทึกเนื้อหาไฟล์ตามลำดับที่อ่าน ตัวอย่างเช่นหากไฟล์ขนาดเล็กจริงๆอ่านเสร็จก่อนไฟล์ขนาดใหญ่จริง ๆ ไฟล์นั้นจะถูกบันทึกก่อนแม้ว่าไฟล์ขนาดเล็กจะอยู่หลังไฟล์ขนาดใหญ่ในfilesอาเรย์ อย่างไรก็ตามในวิธีการของฉันด้านบนคุณรับประกันได้ว่าคอนโซลจะเข้าสู่ระบบไฟล์ในลำดับเดียวกันกับอาเรย์ที่ให้ไว้


1
ฉันค่อนข้างมั่นใจว่าคุณไม่ถูกต้อง: ฉันค่อนข้างมั่นใจว่าวิธีการของคุณยังสามารถอ่านไฟล์ตามลำดับได้ ใช่มันจะบันทึกผลลัพธ์ในลำดับที่ถูกต้อง (เนื่องจากawait Promise.all) แต่ไฟล์อาจถูกอ่านในลำดับอื่นซึ่งขัดแย้งกับคำสั่งของคุณ "คุณรับประกันว่าคอนโซลจะบันทึกไฟล์ในลำดับเดียวกันกับที่พวกเขาเป็น อ่าน".
Venryx

1
@Venryx คุณถูกต้องขอบคุณสำหรับการแก้ไข ฉันอัพเดตคำตอบแล้ว
chharvey

5

วิธีการแก้ปัญหาของ Bergiทำงานได้ดีเมื่อfsมีสัญญาตาม คุณสามารถใช้bluebird, fs-extraหรือfs-promiseสำหรับเรื่องนี้

อย่างไรก็ตามโซลูชันสำหรับfs libary ดั้งเดิมของโหนดมีดังนี้:

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') compulsorily รับฟังก์ชั่นเป็นอาร์กิวเมนต์ที่ 3 มิฉะนั้นจะเกิดข้อผิดพลาด:

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
ภาคผนวกเล็กน้อยอย่าลืมห่อรอ / asyncs ของคุณในบล็อกลอง / จับ !!
Jay Edwards

3

ชอบการตอบกลับของ @ Bergi แต่มีข้อแตกต่าง

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 เพื่อห่อรหัสบทบาทลงในฟังก์ชั่น async สร้างโมดูลง่าย ๆ ที่ทำทุกอย่างให้คุณหรือคุณสามารถไปในทางที่ถูกต้องนั่นคือการแต่ง

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

โปรดทราบว่าชื่อของตัวแปรเปลี่ยนแปลงเนื่องจากความหมาย คุณผ่าน functor (ฟังก์ชันที่สามารถเรียกใช้โดยฟังก์ชันอื่น) และรับตัวชี้บนหน่วยความจำที่มีบล็อกเริ่มต้นของตรรกะของแอปพลิเคชัน

แต่ถ้าไม่ใช่โมดูลและคุณต้องส่งออกตรรกะหรือไม่

ล้อมฟังก์ชั่นในฟังก์ชั่น async

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

หรือเปลี่ยนชื่อของตัวแปรอะไรก็ตาม ...


* โดยผลข้างเคียงหมายถึงผลกระทบด้าน Colacteral ใด ๆ ของแอปพลิเคชันที่สามารถเปลี่ยนสเตตัส / พฤติกรรมหรือข้อผิดพลาดภายในแอปพลิเคชันเช่น IO

** โดย "บริสุทธิ์" มันอยู่ในเครื่องหมายวรรคตอนเนื่องจากฟังก์ชั่นไม่บริสุทธิ์และรหัสสามารถแปลงเป็นรุ่นบริสุทธิ์เมื่อไม่มีเอาต์พุตคอนโซลเพียงการปรับเปลี่ยนข้อมูลเท่านั้น

นอกจากนี้เพื่อความบริสุทธิ์คุณจะต้องทำงานกับพระที่จัดการกับผลข้างเคียงที่เกิดข้อผิดพลาดได้ง่ายและปฏิบัติต่อข้อผิดพลาดนั้นแยกจากแอปพลิเคชัน


2

ข้อแม้ที่สำคัญอย่างหนึ่งคือ: await + for .. ofวิธีการและวิธีการforEach + asyncที่จริงมีผลกระทบที่แตกต่างกัน

การมีวงวนawaitจริงforจะทำให้แน่ใจได้ว่าการเรียกใช้ async ทั้งหมดจะถูกดำเนินการทีละรายการ และforEach + asyncวิธีจะดับสัญญาทั้งหมดในเวลาเดียวกันซึ่งเร็วกว่า แต่บางครั้งก็ท่วมท้น ( ถ้าคุณทำแบบสอบถาม DB หรือเยี่ยมชมบริการเว็บบางแห่งที่มีข้อ จำกัด ด้านปริมาณและไม่ต้องการโทรออกครั้งละ 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 เพื่อที่จะไม่ปรากฏในสำหรับในวง นอกจากนี้คุณอาจใช้การวนซ้ำแบบเดียวกันกับ native forEach- เข้าถึงดัชนีแทนการพึ่งพา iterability - และส่งผ่าน index ไปยัง callback
Bergi

คุณสามารถใช้Array.prototype.reduceในลักษณะที่ใช้ฟังก์ชั่น async ฉันได้แสดงตัวอย่างในคำตอบของฉันแล้ว: stackoverflow.com/a/49499491/2537258
Timothy Zorn

2

การใช้ Task, futurize และ traversable List คุณสามารถทำได้

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 ที่เสนอ)

import { curry, flip } from 'ramda'

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

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

ป.ล. - ฉันไม่ได้ลองรหัสนี้บนคอนโซลอาจมีความผิดพลาดบางอย่าง ... "ฟรีสไตล์ตรงด้านบนของโดม!" อย่างที่เด็ก 90s พูด :-P


2

ปัจจุบันคุณสมบัติ Array.forEach ต้นแบบไม่สนับสนุนการดำเนินการ async แต่เราสามารถสร้างการเติมโพลีของเราเองเพื่อตอบสนองความต้องการของเรา

// 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 ของคุณจะต้องเป็นฟังก์ชั่นหรือสัญญา async
  • อาร์เรย์ใด ๆ ที่สร้างขึ้นก่อนหน้า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

วันนี้ฉันเจอวิธีแก้ปัญหาหลายอย่างสำหรับเรื่องนี้ การเรียกใช้ async กำลังรอฟังก์ชั่นใน forEach Loop ด้วยการสร้างเสื้อคลุมรอบตัวเราสามารถทำให้สิ่งนี้เกิดขึ้นได้

คำอธิบายโดยละเอียดเพิ่มเติมเกี่ยวกับวิธีการทำงานภายในสำหรับเนทีฟสำหรับแต่ละภาษาและสาเหตุที่ไม่สามารถทำการเรียกใช้ฟังก์ชัน async และรายละเอียดอื่น ๆ เกี่ยวกับวิธีการต่างๆได้ในลิงก์ที่นี่

มีหลายวิธีที่สามารถทำได้และมีดังนี้

วิธีที่ 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

@Bergi: ขอบคุณสำหรับความคิดเห็นที่ถูกต้องคุณช่วยอธิบายหน่อยได้ไหมว่าทำไมวิธีที่ 1 และ 2 ไม่ถูกต้อง มันยังทำหน้าที่ตามวัตถุประสงค์ มันใช้งานได้ดีมาก นี่เป็นการบอกว่าวิธีการทั้งหมดนี้เป็นไปได้โดยขึ้นอยู่กับสถานการณ์ที่สามารถตัดสินใจเลือกได้ ฉันมีตัวอย่างการรันสำหรับสิ่งเดียวกัน
PranavKAndro

มันล้มเหลวในอาร์เรย์ที่ว่างเปล่าไม่มีการจัดการข้อผิดพลาดใด ๆ และอาจมีปัญหามากกว่านี้ อย่าประดิษฐ์ล้อใหม่ Promise.allใช้เพียงแค่
Bergi

ในเงื่อนไขบางอย่างที่เป็นไปไม่ได้มันจะเป็นประโยชน์ นอกจากนี้การจัดการข้อผิดพลาดจะกระทำโดย foriach แต่ละครั้งโดยปริยายดังนั้นจึงไม่มีปัญหา มันได้รับการดูแล!
PranavKAndro

ไม่ไม่มีเงื่อนไขที่Promise.allไม่สามารถทำได้ แต่async/ awaitคือ และไม่forEachอย่างแน่นอนไม่จัดการข้อผิดพลาดของสัญญา
Bergi

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 / await เข้ากันไม่ได้ นี่เป็นเพราะสัญญาที่ส่งคืนจากการโทรกลับ async คาดว่าจะได้รับการแก้ไข แต่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)
}

Senario แรกเหมาะสำหรับลูปที่ต้องวิ่งในเซเรียอาและคุณไม่สามารถใช้สำหรับการ
Mark Odey

0

คล้ายกับ Antonio Val's 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 ที่ส่วนท้ายของวิธีการ

สิ่งที่ผิดปกติ:

  • คำสั่งโดยพลการ
  • printFiles สามารถทำงานให้เสร็จก่อนที่จะพิมพ์ไฟล์
  • ประสิทธิภาพต่ำ

สิ่งเหล่านี้ไม่ได้ผิดเสมอไป แต่บ่อยครั้งอยู่ในกรณีใช้งานมาตรฐาน

โดยทั่วไปแล้วการใช้ 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 ที่ผ่านการทดสอบอย่างดี (ล้านดาวน์โหลดต่อสัปดาห์) ถ้าคุณไม่คุ้นเคยกับโมดูล async ผมขอแนะนำให้คุณตรวจสอบเอกสารของ ฉันเคยเห็นหลาย devs เสียเวลาสร้างวิธีการหรือแย่กว่านั้นทำให้รหัส async ที่ยากต่อการบำรุงรักษาเมื่อวิธี 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 / คอยแทนที่จะเป็น async lib ตอนนี้ฉันคิดว่าแต่ละแห่งมีเวลาและสถานที่ ฉันไม่เชื่อว่า async lib == "callback hell" และ async / await == "ยุค JS สมัยใหม่" imo, เมื่อ async lib> async / คอย: 1. การไหลที่ซับซ้อน (เช่นคิว, สินค้า, แม้อัตโนมัติเมื่อสิ่งต่าง ๆ มีความซับซ้อน) 2. การทำงานพร้อมกัน 3. รองรับอาร์เรย์ / วัตถุ / iterables 4. การจัดการที่ผิด
Zachary Ryan Smith
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.