javascript - example - nodejs async function




Использование async/await с циклом forEach (10)

В дополнение к ответу @ Bergi , я хотел бы предложить третий вариант. Это очень похоже на второй пример @ 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 все равно возвращает объект Promise. Следовательно, promises - это массив объектов Promise, которые можно отправить в Promise.all() .

В ответе @ Bergi консоль может записывать содержимое файла не по порядку. Например, если действительно маленький файл заканчивает чтение перед действительно большим файлом, он сначала регистрируется, даже если маленький файл идет после большого файла в массиве files . Тем не менее, в моем методе выше, вы гарантированно, консоль будет записывать файлы в том же порядке, в котором они читаются

Есть ли проблемы с использованием 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 в функции более высокого порядка, как это, поэтому я просто хотел спросить, есть ли какие-либо проблемы с этим.


В настоящее время свойство прототипа Array.forEach не поддерживает асинхронные операции, но мы можем создать наше собственное poly-fill для удовлетворения наших потребностей.

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

Вот и все! Теперь у вас есть асинхронный метод forEach, доступный для любых массивов, определенных после этих операций.

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

// Nodejs style
// file: someOtherFile.js

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

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

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

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

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

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

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

Array.prototype.asyncMap = asyncMap

... и так далее :)

Некоторые вещи на заметку:

  • Ваша iteratorFunction должна быть асинхронной функцией или обещанием
  • Любые массивы, созданные до Array.prototype.<yourAsyncFunc> = <yourAsyncFunc> не будут иметь эту функцию доступной

Вот несколько прототипов 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));
}

Обратите внимание, что хотя вы можете включать это в свой собственный код, вы не должны включать это в библиотеки, которые вы распространяете среди других (чтобы не загрязнять их глобальные переменные).


Используя Task, Futurize и просматриваемый список, вы можете просто сделать

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

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

Вот как вы это настроите

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

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

Другой способ структурировать нужный код будет

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

Или, возможно, еще более функционально ориентированный

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

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

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

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

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

Тогда из родительской функции

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

Если вам действительно нужна большая гибкость в кодировании, вы можете просто сделать это (для забавы, я использую предложенный оператор Pipe Forward )

import { curry, flip } from 'ramda'

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

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

PS - я не пробовал этот код на консоли, возможно, есть некоторые опечатки ... "прямой фристайл, с верхней части купола!" как сказали бы дети 90-х. :-п


Модуль p-iteration в npm реализует методы итераций Array, поэтому их можно очень просто использовать с async / await.

Пример с вашим делом:

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

Оба вышеупомянутых решения работают, однако, Антонио выполняет работу с меньшим количеством кода, вот как это помогло мне разрешить данные из моей базы данных, от нескольких разных дочерних ссылок, а затем поместить их все в массив и разрешить их в обещании, в конце концов, сделанный:

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

Подобно 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


С ES2018 вы можете значительно упростить все вышеперечисленные ответы на:

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

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

Смотрите спецификацию: https://github.com/tc39/proposal-async-iteration

2018-09-10: Этот ответ в последнее время привлекает большое внимание, пожалуйста, см. Сообщение в блоге Акселя Раушмайера для получения дополнительной информации об асинхронной итерации: http://2ality.com/2016/10/asynchronous-iteration.html


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

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

Решение Берги хорошо работает, когда fs основан на обещаниях. Для этого вы можете использовать bluebird , fs-extra или fs-promise .

Однако решение для нативной библиотеки fs узла выглядит следующим образом:

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

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

      return fileContents;
    }));

Примечание: require('fs') обязательно принимает функцию в качестве 3-го аргумента, в противном случае выдает ошибку:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function




ecmascript-2017