问题
Obviously, given a list l
and an function f
that returns a promise, I could do this:
Promise.all(l.map(f));
The hard part is, I need to map each element, in order. That is, the mapping of the first element must be resolved before the the next one is even started. I want to prevent any parallelism.
I have an idea how to do this, which I will give as an answer, but I am not sure it's a good answer.
Edit: some people are under the impression that since Javascript is itself single-threaded, parallelism is not possible in Javascript.
Consider the following code:
const delay = t => new Promise(resolve => setTimeout(resolve, t));
mapAsync([3000, 2000, 1000], delay).then(n => console.log('beep: ' + n));
A naïve implementation of mapAsync()
would cause "beep" to be printed out once a second for three seconds -- with the numbers in ascending order -- but a correct one would space the beeps out increasingly over six seconds, with the number in descending orders.
For a more practical example, imagine a function that invoked fetch()
and was called on an array of thousands of elements.
Further Edit:
Somebody didn't believe me, so here is the Fiddle.
回答1:
You can't use map()
on its own since you should be able to handle the resolution of the previous Promise. There was a good example of using reduce() for sequencing Promises in an Google article.
reduce() allowes you to "chain" the Promise of the current item with the Promise of the previous item. To start the chain, you pass a resolved Promise as initial value to reduce()
.
Assume l
as the input data and async()
to modify the data asynchronously. It will just multiply the input data by 10.
var l = [1, 2, 3 ,4];
function async(data) {
console.log("call with ", data);
return new Promise((resolve, reject) => {
setTimeout(() => { console.log("resolve", data); resolve(data * 10); }, 1000);
});
}
This is the relevant code (its function is inline-commented)
// Reduce the inout data into a Promise chain
l.reduce(function(sequencePromise, inValue) {
/* For the first item sequencePromise will resolve with the value of the
* Promise.resolve() call passed to reduce(), for all other items it's
* the previous promise that was returned by the handler in the next line.
*/
return sequencePromise.then(function(responseValues) {
/* responseValues is an array, initially it's the empty value passed to
* reduce(), for subsequent calls it's the concat()enation result.
*
* Call async with the current inValue.
*/
return async(inValue).then((outValue) => {
/* and concat the outValue to the
* sequence array and return it. The next item will receive that new
* array as value to the resolver of sequencePromise.
*/
return responseValues.concat([outValue]);
});
});
}, Promise.resolve([]) /* Start with a resolved Promise */ ).then(function(responseValues){
console.log(responseValues);
});
The console will finally log
Array [ 10, 20, 30, 40 ]
回答2:
const mapAsync = (l, f) => new Promise((resolve, reject) => {
const results = [];
const recur = () => {
if (results.length < l.length) {
f(l[results.length]).then(v => {
results.push(v);
recur();
}).catch(reject);
} else {
resolve(results);
}
};
recur();
});
EDIT: Tholle's remark led me to this far more elegant and (I hope) anti-pattern-free solution:
const mapAsync = (l, f) => {
const recur = index =>
index < l.length
? f(l[index]).then(car => recur(index + 1).then(cdr => [car].concat(cdr)))
: Promise.resolve([]);
return recur(0);
};
FURTHER EDIT:
The appropriately named Try-catch-finally suggest an even neater implementation, using reduce
. Further improvements welcome.
const mapAsync2 = (l, f) =>
l.reduce(
(promise, item) =>
promise.then(results =>
f(item).then(result => results.concat([result]))),
Promise.resolve([])
);
回答3:
Rather than coding the logic yourself I'd suggest using async.js for this. Since you're dealing with promises use the promisified async-q library: https://www.npmjs.com/package/async-q (note: the documentation is much easier to read on github:https://github.com/dbushong/async-q)
What you need is mapSeries
:
async.mapSeries(l,f).then(function (result) {
// result is guaranteed to be in the correct order
});
Note that the arguments passed to f
is hardcoded as f(item, index, arr)
. If your function accept different arguments you can always wrap it up in another function to reorder the arguments:
async.mapSeries(l,function(x,idx,l){
return f(x); // must return a promise
}).then(function (result) {
// result is guaranteed to be in the correct order
});
You don't need to do this if your function accepts only one argument.
You can also just use the original callback based async.js:
async.mapSeries(l,function(x,idx,l){
function (cb) {
f(x).then(function(result){
cb(null, result); // pass result as second argument,
// first argument is error
});
}
},function (err, result) {
// result is guaranteed to be in the correct order
});
来源:https://stackoverflow.com/questions/41908706/how-can-a-map-an-asynchronous-function-over-a-list