Sử dụng async / await với vòng lặp forEach


1130

Có bất kỳ vấn đề với việc sử dụng async/ awaittrong một forEachvòng lặp? Tôi đang cố gắng lặp qua một loạt các tệp và awaittrên nội dung của mỗi tệp.

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

Mã này không hoạt động, nhưng điều gì có thể đi sai với điều này? Tôi đã có người nói với tôi rằng bạn không nên sử dụng async/ awaittrong một chức năng bậc cao như thế này, vì vậy tôi chỉ muốn hỏi liệu có vấn đề gì với việc này không.

Câu trả lời:


2152

Chắc chắn mã này hoạt động, nhưng tôi khá chắc chắn rằng nó không làm những gì bạn mong đợi. Nó chỉ thực hiện nhiều cuộc gọi không đồng bộ, nhưng printFileschức năng sẽ ngay lập tức trở lại sau đó.

Đọc theo trình tự

Nếu bạn muốn đọc các tập tin theo trình tự, bạnforEach thực sự không thể sử dụng . Chỉ cần sử dụng một for … ofvòng lặp hiện đại thay vào đó, trong đó awaitsẽ hoạt động như mong đợi:

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

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

Đọc song song

Nếu bạn muốn đọc các tệp song song, bạnforEach thực sự không thể sử dụng . Mỗi cuộc asyncgọi chức năng gọi lại sẽ trả lại một lời hứa, nhưng bạn sẽ ném chúng đi thay vì chờ đợi chúng. Chỉ cần sử dụng mapthay thế và bạn có thể chờ đợi hàng loạt lời hứa mà bạn sẽ nhận được 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
Bạn có thể vui lòng giải thích tại sao for ... of ...làm việc?
Ác quỷ

84
ok tôi biết tại sao ... Sử dụng Babel sẽ biến đổi async/ awaitthành chức năng của trình tạo và sử dụng forEachcó nghĩa là mỗi lần lặp có một hàm tạo riêng lẻ, không liên quan gì đến các hàm khác. vì vậy chúng sẽ được thực hiện độc lập và không có bối cảnh next()với người khác. Trên thực tế, một for()vòng lặp đơn giản cũng hoạt động vì các lần lặp cũng nằm trong một hàm tạo duy nhất.
Demonbane

21
@Demonbane: Tóm lại, vì nó được thiết kế để hoạt động :-) awaitđình chỉ việc đánh giá chức năng hiện tại , bao gồm tất cả các cấu trúc điều khiển. Vâng, nó khá giống với các trình tạo về vấn đề đó (đó là lý do tại sao chúng được sử dụng để polyfill async / await).
Bergi

3
@ arve0 Không thực sự, một asyncchức năng hoàn toàn khác với một Promisecuộc gọi lại thực thi, nhưng có, mapcuộc gọi lại trả về một lời hứa trong cả hai trường hợp.
Bergi

5
Khi bạn đến để tìm hiểu về các lời hứa của JS, nhưng thay vào đó hãy sử dụng nửa giờ dịch tiếng Latin. Hy vọng bạn tự hào @Bergi;)
Félix Gagnon-Grenier

189

Với ES2018, bạn có thể đơn giản hóa rất nhiều tất cả các câu trả lời trên để:

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

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

Xem thông số: đề xuất-async-iteration


2018-09-10: Câu trả lời này đã được chú ý rất nhiều gần đây, vui lòng xem bài đăng trên blog của Axel Rauschmayer để biết thêm thông tin về phép lặp không đồng bộ: ES2018: phép lặp không đồng bộ


4
Được nâng cấp, sẽ rất tuyệt nếu bạn có thể đặt một liên kết đến thông số kỹ thuật trong câu trả lời của mình cho bất kỳ ai muốn biết thêm về phép lặp async.
saadq

8
Không nên là nội dung thay vì tệp trong trình lặp
FluffyBying

10
Tại sao mọi người đang nâng cao câu trả lời này? Hãy xem xét kỹ hơn câu trả lời, câu hỏi và đề xuất. Sau ofnên là hàm async sẽ trả về một mảng. Nó không hoạt động và Francisco nói;
Yevhenii Herasymchuk

3
Hoàn toàn đồng ý với @AntonioVal. Đó không phải là một câu trả lời.
Yevhenii Herasymchuk

2
Mặc dù tôi đồng ý rằng đó không phải là một câu trả lời, nhưng việc nâng cao đề xuất là một cách để tăng mức độ phổ biến của nó có khả năng khiến nó có sẵn sớm hơn để sử dụng sau này.
Robert Molina

61

Thay vì Promise.allkết hợp với Array.prototype.map(không đảm bảo thứ tự Promisegiải quyết), tôi sử dụng Array.prototype.reduce, bắt đầu bằng một giải quyết 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
Điều này hoạt động hoàn hảo, cảm ơn bạn rất nhiều. Bạn có thể giải thích những gì đang xảy ra ở đây với Promise.resolve()await promise;?
parrker9

1
Cái này hay đấy. Tôi có đúng khi nghĩ rằng các tập tin sẽ được đọc theo thứ tự và không phải tất cả cùng một lúc?
GollyJer

1
@ parrker9 Promise.resolve()trả về một đã giải quyết Promiseđối tượng, do đó reducecó một Promiseđể bắt đầu. await promise;sẽ chờ đợi cuối cùng Promisetrong chuỗi để giải quyết. @GollyJer Các tệp sẽ được xử lý tuần tự, từng tệp một.
Timothy Zorn

Sử dụng rất mát mẻ giảm, cảm ơn cho nhận xét! Tôi sẽ chỉ biểu thị rằng, trái ngược với một số phương thức khác được đề cập trong các nhận xét, phương pháp này là đồng bộ, có nghĩa là các tệp được đọc lần lượt và không song song (vì lần lặp tiếp theo của hàm giảm phụ thuộc vào trước đó Lặp lại, nó phải được đồng bộ).
Shay Yzhakov

1
@Shay, Ý bạn là tuần tự, không đồng bộ. Điều này vẫn không đồng bộ - nếu những thứ khác được lên lịch, chúng sẽ chạy ở giữa các lần lặp ở đây.
Timothy Zorn

32

- đun lặp p trên npm thực hiện các phương thức lặp Array để chúng có thể được sử dụng theo cách rất đơn giản với async / await.

Một ví dụ với trường hợp của bạn:

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
Tôi thích điều này vì nó có các hàm / phương thức giống như chính JS - trong trường hợp của tôi, tôi cần somechứ không phải forEach. Cảm ơn!
mikemaccana

25

Dưới đây là một số forEachAsyncnguyên mẫu. Lưu ý bạn sẽ cần đến awaitchúng:

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

Lưu ý trong khi bạn có thể bao gồm điều này trong mã của riêng bạn, bạn không nên đưa điều này vào các thư viện bạn phân phối cho người khác (để tránh gây ô nhiễm toàn cầu của họ).


1
Mặc dù tôi ngần ngại thêm mọi thứ trực tiếp vào nguyên mẫu, nhưng đây là một cách thực hiện async đẹp mắt
DaniOcean

2
Miễn là tên này là duy nhất trong tương lai (như tôi sẽ sử dụng _forEachAsync) thì điều này là hợp lý. Tôi cũng nghĩ rằng đó là câu trả lời hay nhất vì nó tiết kiệm rất nhiều mã soạn sẵn.
mikemaccana

1
@estus Đó là để tránh gây ô nhiễm mã của người khác. Nếu mã thuộc về tổ chức cá nhân của chúng tôi và toàn cầu nằm trong một tệp được xác định rõ ( globals.jssẽ tốt), chúng tôi có thể thêm toàn cầu theo ý muốn.
mikemaccana

1
@mikemaccana Đó là để tránh các thực hành xấu được chấp nhận chung. Điều đó đúng, điều này có thể được thực hiện miễn là bạn chỉ sử dụng mã của bên thứ nhất, điều này hiếm khi xảy ra. Vấn đề là khi bạn sử dụng lib của bên thứ ba, có thể có một số người khác cũng cảm thấy như vậy và sửa đổi cùng một thế giới, chỉ vì nó có vẻ là một ý tưởng tốt vào thời điểm khi lib được viết.
Estus Flask

1
@estus Chắc chắn rồi. Tôi đã thêm một cảnh báo cho câu hỏi để lưu cuộc thảo luận (không đặc biệt hiệu quả) ở đây.
mikemaccana

6

Ngoài câu trả lời của @ Bergi , tôi muốn đưa ra giải pháp thay thế thứ ba. Nó rất giống với ví dụ thứ 2 của @ Bergi, nhưng thay vì chờ đợi từng readFilecá nhân, bạn tạo ra một loạt các lời hứa, mỗi điều bạn chờ đợi ở cuối.

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

Lưu ý rằng hàm được truyền vào .map()không cần thiết async, vì fs.readFiledù sao cũng trả về một đối tượng Promise. Do đó, promisesmột mảng các đối tượng Promise, có thể được gửi đến Promise.all().

Trong câu trả lời của @ Bergi, bảng điều khiển có thể ghi lại nội dung tệp theo thứ tự chúng được đọc. Ví dụ: nếu một tệp thực sự nhỏ kết thúc việc đọc trước một tệp thực sự lớn, nó sẽ được ghi lại trước, ngay cả khi tệp nhỏ xuất hiện sau tệp lớn trong filesmảng. Tuy nhiên, trong phương pháp của tôi ở trên, bạn được đảm bảo rằng bàn điều khiển sẽ ghi lại các tệp theo thứ tự giống như mảng được cung cấp.


1
Tôi khá chắc chắn rằng bạn không chính xác: Tôi khá chắc chắn rằng phương pháp của bạn cũng có thể đọc các tệp không theo thứ tự. Có, nó sẽ ghi nhật ký đầu ra theo đúng thứ tự (do await Promise.all), nhưng các tệp có thể đã được đọc theo một thứ tự khác, điều này mâu thuẫn với tuyên bố của bạn "bạn được đảm bảo rằng bàn điều khiển sẽ ghi các tệp theo thứ tự như chúng đọc".
Venryx

1
@Venryx Bạn nói đúng, cảm ơn vì đã sửa. Tôi đã cập nhật câu trả lời của mình.
chharvey

5

Giải pháp của Bergi hoạt động độc đáo khi fsđược hứa hẹn dựa trên. Bạn có thể sử dụng bluebird, fs-extrahoặc fs-promisecho việc này.

Tuy nhiên, giải pháp chofs libary bản địa của nút như sau:

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

Lưu ý: require('fs') bắt buộc có chức năng là đối số thứ 3, nếu không sẽ ném lỗi:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

4

Tuy nhiên, cả hai giải pháp trên đều hoạt động với công việc ít mã hơn, đây là cách nó giúp tôi giải quyết dữ liệu từ cơ sở dữ liệu của mình, từ một số giới thiệu con khác nhau và sau đó đẩy tất cả vào một mảng và giải quyết nó theo lời hứa làm xong:

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

Thật khó để bật một vài phương thức trong một tệp sẽ xử lý dữ liệu không đồng bộ theo thứ tự tuần tự và mang lại hương vị thông thường hơn cho mã của bạn. Ví dụ:

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

bây giờ, giả sử rằng đã được lưu tại './myAsync.js', bạn có thể làm điều gì đó tương tự như dưới đây trong một tệp liền kề:

...
/* 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
Phụ lục nhỏ, đừng quên bọc await / asyncs của bạn trong các khối thử / bắt !!
Jay Edwards

3

Giống như phản ứng của @ Bergi, nhưng với một sự khác biệt.

Promise.all từ chối tất cả các lời hứa nếu một người bị từ chối.

Vì vậy, sử dụng một đệ quy.

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

readFilesQueuenằm ngoài printFilesnguyên nhân gây ra tác dụng phụ * được giới thiệu bởi console.log, tốt hơn là nên chế giễu, kiểm tra và hoặc gián điệp, vì vậy, thật tuyệt khi có chức năng trả về nội dung (sidenote).

Do đó, mã có thể được thiết kế đơn giản theo đó: ba hàm riêng biệt "thuần túy" ** và không có tác dụng phụ, xử lý toàn bộ danh sách và có thể dễ dàng sửa đổi để xử lý các trường hợp thất bại.

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)

Chỉnh sửa trong tương lai / trạng thái hiện tại

Node hỗ trợ sự chờ đợi cấp cao nhất (điều này chưa có plugin, sẽ không có và có thể được kích hoạt thông qua các cờ hài hòa), thật tuyệt nhưng không giải quyết được một vấn đề (về mặt chiến lược tôi chỉ làm việc trên các phiên bản LTS). Làm thế nào để có được các tập tin?

Sử dụng thành phần. Đưa ra mã, gây cho tôi cảm giác rằng đây là bên trong một mô-đun, vì vậy, nên có một chức năng để làm điều đó. Nếu không, bạn nên sử dụng IIFE để bọc mã vai trò thành hàm async tạo mô-đun đơn giản phù hợp với bạn hoặc bạn có thể đi đúng cách, có, thành phần.

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

Lưu ý rằng tên của các biến thay đổi do ngữ nghĩa. Bạn vượt qua một functor (một hàm có thể được gọi bởi một hàm khác) và nhận một con trỏ trên bộ nhớ có chứa khối logic ban đầu của ứng dụng.

Nhưng, nếu không phải là một mô-đun và bạn cần xuất logic?

Bọc các chức năng trong một chức năng không đồng bộ.

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

Hoặc thay đổi tên của các biến, bất cứ điều gì ...


* bởi tác dụng phụ sẽ tạo ra bất kỳ hiệu ứng vi khuẩn nào của ứng dụng có thể thay đổi trạng thái / hành vi hoặc lỗi xâm nhập trong ứng dụng, như IO.

** bởi "thuần túy", đó là dấu nháy đơn vì các chức năng không thuần túy và mã có thể được hội tụ thành phiên bản thuần túy, khi không có đầu ra giao diện điều khiển, chỉ có các thao tác dữ liệu.

Bên cạnh đó, để thuần túy, bạn sẽ cần phải làm việc với các đơn vị xử lý hiệu ứng phụ, dễ bị lỗi và xử lý riêng lỗi đó của ứng dụng.


2

Một cảnh báo quan trọng là: await + for .. ofPhương pháp và forEach + asynccách thực sự có hiệu quả khác nhau.

awaitbên trong một forvòng lặp thực sẽ đảm bảo tất cả các cuộc gọi async được thực hiện từng cái một. Và forEach + asynccách này sẽ loại bỏ tất cả các lời hứa cùng một lúc, nhanh hơn nhưng đôi khi bị quá tải ( nếu bạn thực hiện một số truy vấn DB hoặc truy cập một số dịch vụ web có giới hạn âm lượng và không muốn thực hiện 100.000 cuộc gọi cùng một lúc).

Bạn cũng có thể sử dụng reduce + promise(kém thanh lịch) nếu bạn không sử dụng async/awaitvà muốn đảm bảo các tệp được đọc lần lượt .

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

Hoặc bạn có thể tạo forEachAsync để trợ giúp nhưng về cơ bản sử dụng tương tự cho vòng lặp bên dưới.

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

Hãy xem Cách xác định phương thức trong javascript trên Array.prototype và Object.prototype để nó không xuất hiện trong vòng lặp . Ngoài ra, bạn có thể nên sử dụng cùng một lần lặp như bản gốc forEach- truy cập các chỉ mục thay vì dựa vào tính lặp lại - và chuyển chỉ mục cho cuộc gọi lại.
Bergi

Bạn có thể sử dụng Array.prototype.reducetheo cách sử dụng chức năng async. Tôi đã đưa ra một ví dụ trong câu trả lời của mình: stackoverflow.com/a/49499491/2537258
Timothy Zorn

2

Sử dụng Nhiệm vụ, tương lai và Danh sách có thể duyệt được, bạn chỉ cần làm

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

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

Đây là cách bạn thiết lập điều này

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)

Một cách khác để cấu trúc mã mong muốn là

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

Hoặc thậm chí có thể định hướng chức năng nhiều hơn

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

Sau đó, từ chức năng cha

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

Nếu bạn thực sự muốn linh hoạt hơn trong mã hóa, bạn có thể thực hiện việc này (để giải trí, tôi đang sử dụng toán tử Chuyển tiếp ống được đề xuất )

import { curry, flip } from 'ramda'

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

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

Tái bút - Tôi đã không thử mã này trên bảng điều khiển, có thể có một số lỗi chính tả ... "tự do thẳng, ra khỏi đỉnh của mái vòm!" như những đứa trẻ thập niên 90 sẽ nói. :-p


2

Hiện tại thuộc tính nguyên mẫu Array.forEach không hỗ trợ các hoạt động không đồng bộ, nhưng chúng tôi có thể tạo poly-fill của riêng mình để đáp ứng nhu cầu của chúng tôi.

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

Và đó là nó! Bây giờ bạn có một phương thức async forEach có sẵn trên bất kỳ mảng nào được xác định sau các hoạt động này.

Hãy thử xem ...

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

Chúng ta có thể làm tương tự cho một số hàm mảng khác như 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

... và như thế :)

Một số điều cần lưu ý:

  • Trình lặp của bạn phải là một chức năng hoặc lời hứa không đồng bộ
  • Bất kỳ mảng nào được tạo trước đó Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>sẽ không có tính năng này

2

Chỉ cần thêm vào câu trả lời ban đầu

  • Cú pháp đọc song song trong câu trả lời ban đầu đôi khi khó hiểu và khó đọc, có lẽ chúng ta có thể viết nó theo một cách tiếp cận khác
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);
}
  • Đối với hoạt động tuần tự, không chỉ cho ... của , vòng lặp thông thường cũng sẽ hoạt động
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

Để xem điều đó có thể sai như thế nào, hãy in console.log ở cuối phương thức.

Những điều có thể đi sai nói chung:

  • Trật tự tùy ý.
  • printFiles có thể chạy xong trước khi in tập tin.
  • Hiệu suất kém.

Những điều này không phải lúc nào cũng sai nhưng thường là trong các trường hợp sử dụng tiêu chuẩn.

Nói chung, sử dụng forEach sẽ dẫn đến tất cả trừ cuối cùng. Nó sẽ gọi từng chức năng mà không cần chờ chức năng nghĩa là nó báo cho tất cả các chức năng bắt đầu sau đó kết thúc mà không cần chờ các chức năng kết thúc.

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

Đây là một ví dụ trong JS gốc sẽ giữ trật tự, ngăn chức năng trả về sớm và trên lý thuyết duy trì hiệu suất tối ưu.

Điều này sẽ:

  • Bắt đầu tất cả các tệp đọc xảy ra song song.
  • Giữ nguyên thứ tự thông qua việc sử dụng bản đồ để ánh xạ tên tệp để hứa hẹn chờ đợi.
  • Đợi cho mỗi lời hứa theo thứ tự được xác định bởi mảng.

Với giải pháp này, tập tin đầu tiên sẽ được hiển thị ngay khi có sẵn mà không phải chờ đợi những tập tin khác có sẵn trước.

Nó cũng sẽ tải tất cả các tệp cùng một lúc thay vì phải đợi tệp đầu tiên kết thúc trước khi có thể bắt đầu đọc tệp thứ hai.

Điểm hạn chế duy nhất của phiên bản này và phiên bản gốc là nếu nhiều lần đọc được bắt đầu cùng một lúc thì việc xử lý lỗi trên tài khoản có nhiều lỗi có thể xảy ra cùng một lúc sẽ khó khăn hơn.

Với các phiên bản đọc một tệp tại một thời điểm sau đó sẽ dừng lại do lỗi mà không mất thời gian cố gắng đọc thêm bất kỳ tệp nào. Ngay cả với một hệ thống hủy phức tạp, khó có thể tránh được lỗi trên tệp đầu tiên nhưng cũng đã đọc hầu hết các tệp khác.

Hiệu suất không phải lúc nào cũng có thể dự đoán. Trong khi nhiều hệ thống sẽ nhanh hơn với các tệp song song, một số sẽ thích tuần tự. Một số là động và có thể thay đổi theo tải, tối ưu hóa cung cấp độ trễ không phải lúc nào cũng mang lại thông lượng tốt trong sự tranh chấp nặng nề.

Cũng không có xử lý lỗi trong ví dụ đó. Nếu một cái gì đó yêu cầu tất cả chúng phải được hiển thị thành công hoặc hoàn toàn không làm điều đó.

Thử nghiệm chuyên sâu được khuyến nghị với console.log ở mỗi giai đoạn và giải pháp đọc tệp giả mạo (thay vào đó là độ trễ ngẫu nhiên). Mặc dù nhiều giải pháp dường như làm tương tự trong các trường hợp đơn giản, tất cả đều có những khác biệt tinh tế cần có sự xem xét kỹ lưỡng hơn để vắt kiệt sức.

Sử dụng giả này để giúp cho sự khác biệt giữa các giải pháp:

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

1

Hôm nay tôi đã đi qua nhiều giải pháp cho việc này. Chạy async đang chờ các chức năng trong Vòng lặp forEach. Bằng cách xây dựng trình bao bọc xung quanh chúng ta có thể thực hiện điều này.

Giải thích chi tiết hơn về cách thức hoạt động bên trong, cho forEach bản địa và lý do tại sao nó không thể thực hiện cuộc gọi hàm async và các chi tiết khác về các phương thức khác nhau được cung cấp trong liên kết tại đây

Nhiều cách mà nó có thể được thực hiện và chúng như sau,

Cách 1: Sử dụng trình bao bọc.

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

Phương pháp 2: Sử dụng giống như một hàm chung của 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');
            }
        })
      });
    };
  }

Sử dụng :

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

Cách 3:

Sử dụng Promise.all

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

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

Phương pháp 4: Vòng lặp truyền thống hoặc vòng lặp hiện đại

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

Phương pháp 1 và 2 của bạn chỉ đơn giản là các triển khai không chính xác Promise.allnên được sử dụng - chúng không tính đến bất kỳ trường hợp nào trong số nhiều trường hợp cạnh.
Bergi

@Bergi: Cảm ơn các bình luận hợp lệ, Bạn vui lòng giải thích cho tôi tại sao phương pháp 1 và 2 không chính xác. Nó cũng phục vụ mục đích. Điều này hoạt động rất tốt. Điều này có nghĩa là tất cả các phương pháp này đều có thể, dựa trên tình huống người ta có thể quyết định chọn một phương pháp. Tôi có ví dụ chạy cho cùng.
PranavKAndro

Nó không thành công trên các mảng trống, nó không có bất kỳ xử lý lỗi nào và có thể có nhiều vấn đề hơn. Đừng phát minh lại bánh xe. Chỉ cần sử dụng Promise.all.
Bergi

Trong một số điều kiện nhất định, nơi nó không thể, nó sẽ hữu ích. Ngoài ra xử lý lỗi được thực hiện bởi forEach api theo mặc định nên không có vấn đề gì. Nó được chăm sóc!
PranavKAndro

Không, không có điều kiện Promise.alllà không thể nhưng async/ awaitlà. Và không, forEachhoàn toàn không xử lý bất kỳ lỗi hứa hẹn.
Bergi

1

Giải pháp này cũng được tối ưu hóa bộ nhớ để bạn có thể chạy nó trên 10.000 mục dữ liệu và yêu cầu. Một số giải pháp khác ở đây sẽ đánh sập máy chủ trên các tập dữ liệu lớn.

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

Sử dụng như thế nào?

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

1

Bạn có thể sử dụng Array.prototype.forEach, nhưng async / await không tương thích. Điều này là do lời hứa được trả về từ một cuộc gọi lại async dự kiến ​​sẽ được giải quyết, nhưng Array.prototype.forEachkhông giải quyết bất kỳ lời hứa nào từ việc thực hiện cuộc gọi lại của nó. Vì vậy, sau đó, bạn có thể sử dụng forEach, nhưng bạn sẽ phải tự xử lý độ phân giải lời hứa.

Đây là một cách để đọc và in từng tệp trong chuỗi bằng cách sử dụng 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
}

Đây là một cách (vẫn đang sử dụng Array.prototype.forEach) để in song song nội dung của tệp

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 đầu tiên là lý tưởng cho các vòng lặp cần được chạy trong serie và bạn không thể sử dụng cho
Mark Odey

0

Tương tự như của Antonio Val p-iteration, một mô-đun npm thay thế là 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();

Ngoài ra, async-afcó một phương thức tĩnh (log / logAF) để ghi lại kết quả của các lời hứa:

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

Tuy nhiên, ưu điểm chính của thư viện là bạn có thể xâu chuỗi các phương thức không đồng bộ để thực hiện một số thứ như:

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


-3

Tôi sẽ sử dụng các mô-đun pifyasync được kiểm tra tốt (hàng triệu lượt tải xuống mỗi tuần) . Nếu bạn không quen thuộc với mô-đun async, tôi khuyên bạn nên kiểm tra tài liệu của nó . Tôi đã thấy nhiều nhà phát triển lãng phí thời gian để tạo lại các phương thức của mình hoặc tệ hơn là tạo mã async khó bảo trì khi các phương thức async bậc cao hơn sẽ đơn giản hóa mã.

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


Đây là một bước đi sai hướng. Đây là một hướng dẫn lập bản đồ mà tôi đã tạo để giúp mọi người bị mắc kẹt trong cuộc gọi lại vào thời đại JS hiện đại: github.com/jmjpro/async-package-to-async-await/blob/master/ trộm .
jbustamovej

như bạn có thể thấy ở đây , tôi quan tâm và mở ra để sử dụng async / await thay vì lib async. Ngay bây giờ, tôi nghĩ rằng mỗi người có một thời gian và địa điểm. Tôi không tin rằng async lib == "địa ngục gọi lại" và async / await == "kỷ nguyên JS hiện đại". imo, khi async lib> async / await: 1. luồng phức tạp (ví dụ: hàng đợi, hàng hóa, thậm chí tự động khi mọi thứ trở nên phức tạp) 2. đồng thời 3. hỗ trợ mảng / đối tượng / iterables 4. xử lý lỗi
Zachary Ryan Smith
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.