JavaScript-Array .reduzieren mit async / await


76

Es scheint einige Probleme zu geben, die async / await mit .reduce () beinhalten, wie zum Beispiel:

const data = await bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName]
  if (methodName == 'foo') {
    current.cover = await this.store(current.cover, id)
    console.log(current)
    return {
      ...accum,
      ...current
    }
  }
  return {
    ...accum,
    ...method(current.data)
  }
}, {})
console.log(data)

Das dataObjekt wird protokolliert, bevor der Vorgang this.storeabgeschlossen ist ...

Ich weiß, dass Sie Promise.allmit asynchronen Schleifen verwenden können, aber gilt das für .reduce()?

Antworten:


132

Das Problem ist, dass Ihre Akkumulatorwerte Versprechen sind - sie sind Rückgabewerte von async functions. Um eine sequentielle Auswertung zu erhalten (und alle bis auf die letzte Iteration, auf die überhaupt gewartet werden muss), müssen Sie verwenden

const data = await array.reduce(async (accumP, current, index) => {
  const accum = await accumP;
  …
}, Promise.resolve(…));

Das heißt, für async/ awaitIch würde im Allgemeinen empfehlen, einfache Schleifen anstelle von Array-Iterationsmethoden zu verwenden , sie sind leistungsfähiger und oft einfacher.


3
Vielen Dank für Ihren Rat am Ende. Am Ende benutzte ich nur eine einfache for-Schleife für das, was ich tat, und es waren die gleichen Codezeilen, aber viel einfacher zu lesen ...
cs_pupil

2
Der initialValuevon dem reducebraucht keine sein Promise, es wird jedoch in den meisten Fällen klärt die Absicht.
EECOLOR

@EECOLOR Es sollte aber sein. Ich mag awaites wirklich nicht , einen einfachen Wert in ein Versprechen
verwandeln zu müssen

@EECOLOR Bei Verwendung von TypeScript muss der Anfangswert ein Versprechen sein, da der Rückgabetyp des Rückrufs immer mit dem Typ des Akkumulators übereinstimmen muss.
jessedvrs

@jessedvrs Ich denke du meinst den Anfangswert (wenn nicht, verstehe ich wahrscheinlich falsch, was du sagst). Sie könnten bestehen, nullnicht wahr?
EECOLOR

5

Ich mag Bergis Antwort, ich denke, es ist der richtige Weg.

Ich möchte auch eine meiner Bibliotheken namens Awaity.js erwähnen

Was können Sie mühelos Funktionen wie reduce, mapund filtermit async / await:

import reduce from 'awaity/reduce';

const posts = await reduce([1,2,3], async (posts, id) => {

  const res = await fetch('/api/posts/' + id);
  const post = await res.json();

  return {
    ...posts,
    [id]: post
  };
}, {})

posts // { 1: { ... }, 2: { ... }, 3: { ... } }

Wird jeder Durchgang sequentiell sein? Oder ruft all diese Wartefunktionen in einem Stapel auf?
wle8300

1
Sequentiell, da jede Iteration vom Rückgabewert der vorherigen abhängt
Asaf Katz

2

Sie können Ihre gesamte Map / Reduce-Iterator-Blöcke in ihre eigene Promise.resolve einbinden und darauf warten, dass diese abgeschlossen ist. Das Problem ist jedoch, dass der Akkumulator nicht die resultierenden Daten / Objekte enthält, die Sie bei jeder Iteration erwarten würden. Aufgrund der internen Kette async / await / Promise handelt es sich bei dem Akkumulator um tatsächliche Promises selbst, die sich wahrscheinlich noch nicht aufgelöst haben, obwohl Sie vor Ihrem Aufruf des Geschäfts ein Schlüsselwort await verwendet haben (was möglicherweise zu der Annahme führt, dass die Iteration nicht tatsächlich erfolgt kehren Sie zurück, bis dieser Aufruf abgeschlossen ist und der Akku aktualisiert wurde.

Dies ist zwar nicht die eleganteste Lösung, Sie haben jedoch die Möglichkeit, Ihre Datenobjektvariable aus dem Gültigkeitsbereich zu verschieben und als Let zuzuweisen, damit eine ordnungsgemäße Bindung und Mutation erfolgen kann. Aktualisieren Sie dann dieses Datenobjekt in Ihrem Iterator, wenn die Aufrufe asynchron / warten / versprechen aufgelöst werden.

/* allow the result object to be initialized outside of scope 
   rather than trying to spread results into your accumulator on iterations, 
   else your results will not be maintained as expected within the 
   internal async/await/Promise chain.
*/    
let data = {}; 

await Promise.resolve(bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName];
  if (methodName == 'foo') {
    // note: this extra Promise.resolve may not be entirely necessary
    const cover = await Promise.resolve(this.store(current.cover, id));
    current.cover = cover;
    console.log(current);
    data = {
      ...data,
      ...current,
    };
    return data;
  }
  data = {
    ...data,
    ...method(current.data)
  };
  return data;
}, {});
console.log(data);

0

export const addMultiTextData = async(data) => {
  const textData = await data.reduce(async(a, {
    currentObject,
    selectedValue
  }) => {
    const {
      error,
      errorMessage
    } = await validate(selectedValue, currentObject);
    return {
      ...await a,
      [currentObject.id]: {
        text: selectedValue,
        error,
        errorMessage
      }
    };
  }, {});
};


3
Während dieses Code-Snippet die Frage lösen kann, hilft eine Erklärung wirklich dabei, die Qualität Ihres Beitrags zu verbessern. Denken Sie daran, dass Sie die Frage in Zukunft für Leser beantworten und diese Personen möglicherweise die Gründe für Ihren Codevorschlag nicht kennen.
Shree

Um nicht zu sagen, ich würde diesen Ansatz nicht einmal empfehlen, da die Verwendung von Spread-Operatoren in Schleifen sehr leistungsintensiv ist.
Robert Molina

0

[OPs nicht genau ansprechen; konzentrierte sich auf andere, die hier landen.]

Reduzieren wird häufig verwendet, wenn Sie das Ergebnis der vorherigen Schritte benötigen, bevor Sie das nächste verarbeiten können. In diesem Fall können Sie Versprechen a la aneinander reihen:

promise = elts.reduce(
    async (promise, elt) => {
        return promise.then(async last => {
            return await f(last, elt)
        })
    }, Promise.resolve(0)) // or "" or [] or ...

Hier ist ein Beispiel mit fs.promise.mkdir () (sicher, viel einfacher, mkdirSync zu verwenden, aber in meinem Fall ist es über ein Netzwerk):

const Path = require('path')
const Fs = require('fs')

async function mkdirs (path) {
    return path.split(/\//).filter(d => !!d).reduce(
        async (promise, dir) => {
            return promise.then(async parent => {
                const ret = Path.join(parent, dir);
                try {
                    await Fs.promises.lstat(ret)
                } catch (e) {
                    console.log(`mkdir(${ret})`)
                    await Fs.promises.mkdir(ret)
                }
                return ret
            })
        }, Promise.resolve(""))
}

mkdirs('dir1/dir2/dir3')

Unten ist ein weiteres Beispiel, das 100 + 200 ... 500 addiert und ein bisschen wartet:

async function slowCounter () {
    const ret = await ([100, 200, 300, 400, 500]).reduce(
        async (promise, wait, idx) => {
            return promise.then(async last => {
                const ret = last + wait
                console.log(`${idx}: waiting ${wait}ms to return ${ret}`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            })
        }, Promise.resolve(0))
    console.log(ret)
}

slowCounter ()


0

So reduzieren Sie Async:

async function asyncReduce(arr, fn, initialValue) {
  let temp = initialValue;

  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    temp = await fn(temp, cur, idx);
  }

  return temp;
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.