如何在列表上映射异步函数?

时间:2017-01-28 10:28:52

标签: javascript es6-promise

显然,给定一个返回承诺的列表l和一个函数f,我可以这样做:

Promise.all(l.map(f));

困难的部分是,我需要按顺序映射每个元素 。也就是说,必须在下一个元素的开始之前解析第一个元素的映射。我想阻止任何并行性。

我知道如何做到这一点,我会给出答案,但我不确定这是一个好答案。

编辑:有些人认为,由于Javascript本身是单线程的,因此在Javascript中无法实现并行性。

请考虑以下代码:

const delay = t => new Promise(resolve => setTimeout(resolve, t));
mapAsync([3000, 2000, 1000], delay).then(n => console.log('beep: ' + n));

mapAsync()的天真实现会导致“哔哔”每秒打印一次,持续三秒钟 - 数字按升序排列 - 但正确会有空格发出的声音越来越多,超过6秒,数字按降序排列。

对于一个更实际的例子,想象一个调用fetch()并在数千个元素的数组上调用的函数。

进一步修改:

有人不相信我,所以here是小提琴。

3 个答案:

答案 0 :(得分:1)

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

编辑: Tholle的评论让我更加优雅和(我希望)anti-pattern - 免费解决方案:

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

进一步编辑

适当命名的Try-catch-finally最终建议使用reduce进行更整洁的实现。欢迎进一步改进。

const mapAsync2 = (l, f) =>
  l.reduce(
    (promise, item) =>
      promise.then(results => 
        f(item).then(result => results.concat([result]))),
    Promise.resolve([])
  );

答案 1 :(得分:1)

不是自己编写逻辑,而是建议使用async.js。由于您正在处理promises,请使用promisified async-q库:https://www.npmjs.com/package/async-q(注意:文档在github上更容易阅读:https://github.com/dbushong/async-q

您需要的是mapSeries

async.mapSeries(l,f).then(function (result) {
    // result is guaranteed to be in the correct order
});

请注意,传递给f的参数被硬编码为f(item, index, arr)。如果你的函数接受不同的参数,你总是可以将它包装在另一个函数中以重新排序参数:

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

如果您的函数只接受一个参数,则不需要这样做。

您也可以使用基于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
});

答案 2 :(得分:0)

由于您应该能够处理先前Promise的解决方案,因此您无法单独使用map()。有一个很好的example of using reduce() for sequencing Promises in an Google article

reduce()允许您使用前一项的Promise“链接”当前项目的Promise。要启动链,请将已解析的Promise作为初始值传递给reduce()

假设l为输入数据,async()以异步方式修改数据。它只会将输入数据乘以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);
    });
}

这是相关代码(其功能是内联注释的)

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

控制台最终会记录

Array [ 10, 20, 30, 40 ]