一个接一个地(即按顺序)解决Promise?

IT技术 javascript promise q sequential serial-processing
2021-01-14 01:13:57

考虑以下以串行/顺序方式读取文件数组的代码。readFiles返回一个Promise,只有在按顺序读取所有文件后才会解决。

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => {
    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

    readSequential(0); // Start with the first file!
  });
};

上面的代码有效,但我不喜欢为了顺序发生的事情而进行递归。有没有更简单的方法可以重写这段代码,这样我就不必使用我奇怪的readSequential函数?

最初我尝试使用Promise.all,但这导致所有readFile调用同时发生,这不是我想要的:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};
6个回答

2017 年更新:如果环境支持,我会使用异步函数:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

如果你愿意,你可以推迟读取文件,直到你使用异步生成器需要它们(如果你的环境支持它):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

更新:再想一想 - 我可能会改用 for 循环:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

或者更简洁地,使用 reduce:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

在其他Promise库(如 when 和 Bluebird)中,您有用于此的实用程序方法。

例如,蓝鸟将是:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

虽然今天真的没有理由使用 async await 。

2021-03-29 01:13:57
@ArturTagisow TypeScript(至少是新版本)具有递归类型,应该在这里正确解析类型。没有 Promise<Promise<T>> 这样的东西,因为 Promise 是“递归吸收”的。Promise.resolve(Promise.resolve(15))与 相同Promise.resolve(15)
2021-03-30 01:13:57
@EmreTapcı,不。箭头函数的“=>”已经暗示返回。
2021-04-01 01:13:57
@albertjan - 您为我节省了数小时的压力。万分感谢!
2021-04-06 01:13:57
如果您使用 TypeScript,我认为“for in”循环解决方案是最好的。减少返回递归Promise,例如。第一个调用返回类型是 Promise<void>,然后第二个是 Promise<Promise<void>> 等等 - 我认为不使用任何类型是不可能输入的
2021-04-09 01:13:57

这是我更喜欢按顺序运行任务的方式。

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

有更多任务的案例呢?比如,10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}
如果你想减少线路噪音,你也可以写 result = result.then(task);
2021-03-11 01:13:57
如果您知道任务数量,但仅在运行时呢?
2021-03-13 01:13:57
@DanielBuckmaster 是的,但要小心,因为如果 task() 返回一个值,它将被传递到下一次调用。如果您的任务有可选参数,这可能会导致副作用。当前代码吞下结果并显式调用下一个任务,不带参数。
2021-03-13 01:13:57
“你根本不想对一系列Promise进行操作。根据Promise规范,一旦Promise被创建,它就会开始执行。所以你真正想要的是一系列Promise工厂”参见高级错误 #3在这里:pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
2021-03-22 01:13:57
如果您不知道确切的任务数量怎么办?
2021-03-29 01:13:57

这个问题很老了,但我们生活在 ES6 和函数式 JavaScript 的世界中,所以让我们看看如何改进。

因为 Promise 会立即执行,所以我们不能只创建一组 Promise,它们都会并行触发。

相反,我们需要创建一个返回Promise的函数数组。然后每个函数将按顺序执行,然后在里面启动Promise。

我们可以通过几种方法解决这个问题,但我最喜欢的方法是使用reduce.

reduce与 promises 结合使用会有点棘手,所以我将一个衬垫分解成下面一些更小的易消化的食物。

这个函数的本质是使用reduce以 的初始值开始Promise.resolve([]),或者一个包含空数组的Promise。

然后这个Promise将作为 传递到reduce方法中promise这是将每个Promise按顺序链接在一起的关键。下一个要执行的Promise是func,当then触发时,将结果连接起来,然后返回该Promise,reduce并使用下一个Promise函数执行循环。

一旦所有 Promise 都执行完毕,返回的 Promise 将包含每个 Promise 的所有结果的数组。

ES6 示例(一个班轮)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

ES6 示例(分解)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

用法:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))
@joelnet,针对danecando 的评论,我认为reduce 所做的应该更正确地表达在以下表达式中,您同意吗?Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))等等
2021-03-11 01:13:57
这有点难以理解,但减少基本上是正确的吗? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
2021-03-13 01:13:57
@danecando,是的,这看起来是正确的。你也可以在 return 中删除 Promise.resolve,任何返回的值都会被自动解析,除非你对它们调用 Promise.reject。
2021-03-16 01:13:57
非常好,谢谢,Array.prototype.concat.bind(result)是我遗漏的部分,不得不手动推送结果,这有效但不太酷
2021-04-01 01:13:57
由于我们都是关于现代 JS,我相信console.log.bind(console)你最后一个例子中陈述现在通常是不必要的。这几天你就可以过去了console.log例如。serial(funcs).then(console.log). 在当前的 nodejs 和 Chrome 上测试。
2021-04-04 01:13:57

要在 ES6 中简单地做到这一点:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}
似乎它正在使用下划线。您可以简化为files.forEach文件是否为数组。
2021-03-11 01:13:57
嗯……是 ES5。ES6 的方式是for (file of files) {...}.
2021-03-20 01:13:57
@canac 抱歉,这只是一个玩弄文字的玩笑(“空洞的Promise......”)。绝对Promise.resolve();在您的代码中使用
2021-03-23 01:13:57
很好的解决方案,易于遵循。我没有把return sequence;我的放在一个函数中,所以在最后解决而不是把我放在sequence.then(() => { do stuff });
2021-03-25 01:13:57
你说你不应该Promise.resolve()在现实生活中创造一个已经解决的Promise。为什么不?Promise.resolve()似乎比new Promise(success => success()).
2021-04-01 01:13:57

标准 Node.js Promise的简单实用程序:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

更新

items-promise是一个随时可用的 NPM 包,做同样的事情。

我提供了这个答案的变体,并在下面进行了解释。谢谢
2021-03-15 01:13:57
这正是我在无法访问 async/await 的 Node 7 之前的环境中所做的。漂亮干净。
2021-03-30 01:13:57
我很想看到更详细的解释。
2021-04-05 01:13:57