JavaScript-array .reduce met async/wait

Het lijkt erop dat er problemen zijn met het opnemen van async/wait met .reduce(), zoals:

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)

Het data-object wordt gelogd voordatde this.storeis voltooid…

Ik weet dat je Promise.allkunt gebruiken met asynchrone lussen, maar geldt dat ook voor .reduce()?


Antwoord 1, autoriteit 100%

Het probleem is dat uw accumulatorwaarden beloften zijn – het zijn retourwaarden van async functions. Om sequentiële evaluatie te krijgen (en alles behalve de laatste iteratie waarop moet worden gewacht), moet u

gebruiken

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

Dat gezegd hebbende, voor async/awaitzou ik in het algemeen aanraden om gebruik gewone lussen in plaats van array-iteratiemethoden, deze zijn beter presterend en vaak eenvoudiger.


Antwoord 2, autoriteit 3%

Ik hou van Bergi’s antwoord, ik denk dat dit de juiste manier is.

Ik wil ook een bibliotheek van mij noemen, genaamd Awaity.js

Hiermee kunt u moeiteloos functies gebruiken zoals reduce, map& filtermet 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: { ... } }

Antwoord 3, autoriteit 2%

[Niet ingaan op de exacte prob van OP’s; gericht op anderen die hier landen.]

Verminderen wordt vaak gebruikt wanneer u het resultaat van de vorige stappen nodig heeft voordat u de volgende kunt verwerken. In dat geval kun je beloftes a la aan elkaar rijgen:

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

Hier is een voorbeeld met gebruik van fs.promise.mkdir() (zeker, veel eenvoudiger om mkdirSync te gebruiken, maar in mijn geval is het via een netwerk):

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

Hieronder is nog een voorbeeld dat 100 + 200 … 500 toevoegt en een beetje wacht:

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

Snippet uitvouwen


Antwoord 4

Soms is het het beste om gewoon beide codeversies naast elkaar te plaatsen, sync en async:

Synchronisatieversie:

const arr = [1, 2, 3, 4, 5];
const syncRev = arr.reduce((acc, i) => [i, ...acc], []); // [5, 4, 3, 2, 1] 

Asynchrone één:

(async () => { 
   const asyncRev = await arr.reduce(async (promisedAcc, i) => {
      const id = await asyncIdentity(i); // could be id = i, just stubbing async op.
      const acc = await promisedAcc;
      return [id, ...acc];
   }, Promise.resolve([]));   // [5, 4, 3, 2, 1] 
})();
//async stuff
async function asyncIdentity(id) {
   return Promise.resolve(id);
}

Codefragment weergeven


Antwoord 5

Je kunt je hele map/reduce-iteratorblokken in hun eigen Promise.resolve wikkelen en daarop wachten om te voltooien. Het probleem is echter dat de accumulator niet de resulterende data/objecten bevat die je bij elke iteratie zou verwachten. Vanwege de interne asynchrone/wachten/belofte-keten, zal de accumulator daadwerkelijke beloften zijn die zichzelf waarschijnlijk nog moeten oplossen ondanks het gebruik van een wait-trefwoord voordat u naar de winkel belt (wat ertoe kan leiden dat u denkt dat de iteratie niet echt keer terug totdat die oproep is voltooid en de accumulator is bijgewerkt.

Hoewel dit niet de meest elegante oplossing is, is een optie die je hebt om je dataobjectvariabele buiten het bereik te verplaatsen en het toe te wijzen als een letzodat de juiste binding en mutatie kan optreden. Werk dit gegevensobject vervolgens bij vanuit uw iterator als de async/wait/Promise-aanroepen worden opgelost.

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

Antwoord 6

Het huidige geaccepteerde antwoord adviseert om Promise.all()te gebruiken in plaats van een asyncreduce. Dit heeft echter niet hetzelfde gedrag als een asyncreduceen is alleen relevant voor het geval dat u wilt dat een uitzondering alle iteraties onmiddellijk stopt, wat niet altijd het geval is .

Bovendien wordt in de opmerkingen van dat antwoord gesuggereerd dat u altijd op de accumulator moet wachten als de eerste verklaring in de verkleiner, omdat u anders het risico loopt onverwerkte afwijzingen van beloften te riskeren. De poster zegt ook dat dit was waar de OP om vroeg, wat niet het geval is. In plaats daarvan wil hij gewoon weten wanneer alles klaar is. Om te weten dat je inderdaad await accmoet doen, maar dit kan op elk punt in het verloop zijn.

const reducer = async(acc, key) => {
  const response = await api(item);
  return {
    ...await acc, // <-- this would work just as well for OP
    [key]: reponse,
  }
}
const result = await ['a', 'b', 'c', 'd'].reduce(reducer, {});
console.log(result); // <-- Will be the final result

Hoe veilig gebruik te maken van asyncreduce

Dat gezegd hebbende, het gebruik van een verloopstuk op deze manier betekent wel dat je moet garanderen dat het niet weggooit, anders krijg je “onverwerkte afwijzingen van beloften”. Het is perfect mogelijk om dit te garanderen door een try-catchte gebruiken, waarbij het catch-blok de accumulator retourneert (optioneel met een record voor de mislukte API-aanroep).

const reducer = async (acc, key) => {
    try {
        data = await doSlowTask(key);
        return {...await acc, [key]: data};
    } catch (error) {
        return {...await acc, [key]: {error}};
    };
}
const result = await ['a', 'b', 'c','d'].reduce(reducer, {});

Verschil met Promise.allSettled
Je kunt het gedrag van een asyncreduce(met foutopsporing) benaderen door Promise.allSettledte gebruiken. Dit is echter onhandig om te gebruiken: je moet erna nog een synchrone reductie toevoegen als je wilt reduceren tot een object.

De theoretische tijdscomplexiteit is ook hoger voor Promise.allSettled+ reguliere reduce, hoewel er waarschijnlijk maar weinig gebruiksgevallen zijn waarin dit een verschil zal maken. asyncreducekan beginnen op te stapelen vanaf het moment dat het eerste item is gedaan, terwijl een reducena Promise.allSettledis geblokkeerd totdat alle beloften zijn vervuld. Dit kan een verschil maken bij het doorlopen van een zeer groot aantal elementen.

const responseTime = 200; //ms
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
const api = async (key) => {
    console.log(`Calling API for ${ key }`);
    // Boz is a slow endpoint.
    await sleep(key === 'boz' ? 800 : responseTime);
    console.log(`Got response for ${ key }`);
    if (key === 'bar') throw new Error(`It doesn't work for ${ key }`);
    return {
        [key]: `API says ${ key }`,
    };
};
const keys = ['foo', 'bar', 'baz', 'buz', 'boz'];
const reducer = async (acc, key) => {
    let data;
    try {
        const response = await api(key);
        data = {
            apiData: response
        };
    } catch (e) {
        data = {
            error: e.message
        };
    }
    // OP doesn't care how this works, he only wants to know when the whole thing is ready.
    const previous = await acc;
    console.log(`Got previous for ${ key }`);
    return {
        ...previous,
        [key]: {
            ...data
        },
    };
};
(async () => {
    const start = performance.now();
    const result = await keys.reduce(reducer, {});
    console.log(`After ${ performance.now() - start }ms`, result); // <-- OP wants to execute things when it's ready.
})();

Snippet uitvouwen


Antwoord 7

Een andere klassieke optie met Bluebird

const promise = require('bluebird');
promise.reduce([1,2,3], (agg, x) => Promise.resolve(agg+x),0).then(console.log);
// Expected to product sum 6

Antwoord 8

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

Snippet uitvouwen


Antwoord 9

Hier leest u hoe u asynchrone vermindering kunt maken:

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

Other episodes