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.store
is voltooid…
Ik weet dat je Promise.all
kunt 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 function
s. 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
/await
zou 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
& filter
met 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 ()
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);
}
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 async
reduce
. Dit heeft echter niet hetzelfde gedrag als een async
reduce
en 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 acc
moet 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 async
reduce
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-catch
te 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 async
reduce
(met foutopsporing) benaderen door Promise.allSettled
te 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. async
reduce
kan beginnen op te stapelen vanaf het moment dat het eerste item is gedaan, terwijl een reduce
na Promise.allSettled
is 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.
})();
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
}
};
}, {});
};
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;
}