Hier ist meine ES7-Lösung für eine Copy-Paste-freundliche und vollständige Promise.all()
/ map()
alternative Funktion mit einem Parallelitätslimit.
Ähnlich Promise.all()
wird die Rückgabereihenfolge sowie ein Fallback für nicht versprochene Rückgabewerte beibehalten.
Ich habe auch einen Vergleich der verschiedenen Implementierungen beigefügt, da er einige Aspekte veranschaulicht, die einige der anderen Lösungen übersehen haben.
Verwendung
const asyncFn = delay => new Promise(resolve => setTimeout(() => resolve(), delay));
const args = [30, 20, 15, 10];
await asyncPool(args, arg => asyncFn(arg), 4); // concurrency limit of 4
Implementierung
async function asyncBatch(args, fn, limit = 8) {
// Copy arguments to avoid side effects
args = [...args];
const outs = [];
while (args.length) {
const batch = args.splice(0, limit);
const out = await Promise.all(batch.map(fn));
outs.push(...out);
}
return outs;
}
async function asyncPool(args, fn, limit = 8) {
return new Promise((resolve) => {
// Copy arguments to avoid side effect, reverse queue as
// pop is faster than shift
const argQueue = [...args].reverse();
let count = 0;
const outs = [];
const pollNext = () => {
if (argQueue.length === 0 && count === 0) {
resolve(outs);
} else {
while (count < limit && argQueue.length) {
const index = args.length - argQueue.length;
const arg = argQueue.pop();
count += 1;
const out = fn(arg);
const processOut = (out, index) => {
outs[index] = out;
count -= 1;
pollNext();
};
if (typeof out === 'object' && out.then) {
out.then(out => processOut(out, index));
} else {
processOut(out, index);
}
}
}
};
pollNext();
});
}
Vergleich
// A simple async function that returns after the given delay
// and prints its value to allow us to determine the response order
const asyncFn = delay => new Promise(resolve => setTimeout(() => {
console.log(delay);
resolve(delay);
}, delay));
// List of arguments to the asyncFn function
const args = [30, 20, 15, 10];
// As a comparison of the different implementations, a low concurrency
// limit of 2 is used in order to highlight the performance differences.
// If a limit greater than or equal to args.length is used the results
// would be identical.
// Vanilla Promise.all/map combo
const out1 = await Promise.all(args.map(arg => asyncFn(arg)));
// prints: 10, 15, 20, 30
// total time: 30ms
// Pooled implementation
const out2 = await asyncPool(args, arg => asyncFn(arg), 2);
// prints: 20, 30, 15, 10
// total time: 40ms
// Batched implementation
const out3 = await asyncBatch(args, arg => asyncFn(arg), 2);
// prints: 20, 30, 20, 30
// total time: 45ms
console.log(out1, out2, out3); // prints: [30, 20, 15, 10] x 3
// Conclusion: Execution order and performance is different,
// but return order is still identical
Fazit
asyncPool()
sollte die beste Lösung sein, da neue Anforderungen gestartet werden können, sobald eine vorherige abgeschlossen ist.
asyncBatch()
wird als Vergleich aufgenommen, da die Implementierung einfacher zu verstehen ist, die Leistung jedoch langsamer sein sollte, da alle Anforderungen im selben Stapel abgeschlossen sein müssen, um den nächsten Stapel zu starten.
In diesem erfundenen Beispiel ist die nicht begrenzte Vanille Promise.all()
natürlich die schnellste, während die anderen in einem realen Überlastungsszenario eine wünschenswertere Leistung erbringen könnten.
Aktualisieren
Die Async-Pool-Bibliothek, die andere bereits vorgeschlagen haben, ist wahrscheinlich eine bessere Alternative zu meiner Implementierung, da sie fast identisch funktioniert und eine präzisere Implementierung mit einer cleveren Verwendung von Promise.race () aufweist: https://github.com/rxaviers/ async-pool / blob / master / lib / es7.js
Hoffentlich kann meine Antwort immer noch einen pädagogischen Wert haben.