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

Xåp*_* - 238 javascript promise sequential q serial-processing

请考虑以下以串行/顺序方式读取文件数组的代码.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!

  });
};
Run Code Online (Sandbox Code Playgroud)

上面的代码可以工作,但我不喜欢按顺序进行递归递归.是否有一种更简单的方法可以重写此代码,以便我不必使用我的奇怪readSequential功能?

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

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};
Run Code Online (Sandbox Code Playgroud)

Ben*_*aum 287

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

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};
Run Code Online (Sandbox Code Playgroud)

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

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};
Run Code Online (Sandbox Code Playgroud)

更新:在第二个想法 - 我可能会使用for循环:

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

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};
Run Code Online (Sandbox Code Playgroud)

或者更紧凑,减少:

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

在其他的promise库中(比如when和Bluebird)你有实用的方法.

例如,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.
});
Run Code Online (Sandbox Code Playgroud)

虽然今天没有理由使用异步等待.

  • @EmreTapcı,不。箭头函数的“=>”已经意味着返回。 (2认同)
  • @ArturTagisow TypeScript(至少是新版本)具有递归类型,并且_应该_在此处正确解析类型。由于 Promise“递归同化”,所以不存在 Promise<Promise<T>> 这样的东西。`Promise.resolve(Promise.resolve(15))` 与 `Promise.resolve(15)` 相同。 (2认同)
  • `files.forEach([arrayOfPromises])` 不是连续的,因为 `forEach` 不会等待承诺解决后再执行下一个承诺。“Array.map”也是如此,它也是并发的。 (2认同)

And*_*erg 64

以下是我更喜欢按顺序运行任务的方法.

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 ----");
        });
}
Run Code Online (Sandbox Code Playgroud)

那些有更多任务的案件呢?喜欢,10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}
Run Code Online (Sandbox Code Playgroud)

  • "你根本不想对一系列的承诺进行操作.根据承诺规范,一旦创建了承诺,就会开始执行.所以你真正想要的是一系列承诺工厂"见高级错误#3这里:https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html (9认同)
  • 那么你不知道任务的确切数量呢? (8认同)
  • 如果你要减少线路噪声,你也可以写`result = result.then(task);` (5认同)
  • @DanielBuckmaster 是的,但要小心,因为如果 task() 返回一个值,它将被传递到下一次调用。如果您的任务有可选参数,这可能会导致副作用。当前代码吞下结果并显式调用下一个任务,不带参数。 (3认同)

joe*_*net 57

这个问题很老,但我们生活在ES6和功能JavaScript的世界中,所以让我们看看我们如何改进.

因为promises立即执行,我们不能只创建一个promises数组,它们都会并行启动.

相反,我们需要创建一个返回promise的函数数组.然后每个函数将按顺序执行,然后启动内部的promise.

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

reduce与promises结合使用会变得有点棘手,所以我将下面的一个衬里分解成一些较小的可消化的咬合物.

此函数的本质是使用reduce以初始值start开头Promise.resolve([]),或者使用包含空数组的promise.

然后将此承诺传递给reduce方法as promise.这是按顺序将每个承诺链接在一起的关键.下一个执行的承诺是func,当then触发时,结果被连接,然后返回该承诺,执行reduce具有下一个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([]))
Run Code Online (Sandbox Code Playgroud)

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([]))
Run Code Online (Sandbox Code Playgroud)

用法:

// 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))
Run Code Online (Sandbox Code Playgroud)


Shr*_*pta 35

要在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;
}
Run Code Online (Sandbox Code Playgroud)

  • @canac对不起,这只是一个玩弄文字的笑话("空洞的承诺......").绝对在你的代码中使用`Promise.resolve();`. (8认同)
  • *嗯...*它是ES5.ES6的方式是`for(文件文件){...}`. (2认同)

Poo*_*oya 24

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

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

UPDATE

items-promise是一个随时可用的NPM包.

  • 我很想看到这个更详细的解释. (6认同)

Gil*_*ain 12

使用Async/Await(如果你有ES7的支持)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}
Run Code Online (Sandbox Code Playgroud)

(必须使用for循环,而不是forEach因为 async/await 在 forEach 循环中运行时出现问题)

没有 Async/Await(使用 Promise)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}
Run Code Online (Sandbox Code Playgroud)

  • 不建议在 forEach 内等待。 (5认同)

Sal*_*ter 11

我不得不运行很多顺序任务,并使用这些答案来伪造一个能够处理任何顺序任务的函数......

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}
Run Code Online (Sandbox Code Playgroud)

该函数需要2个参数+ 1个可选项.第一个参数是我们将要工作的数组.第二个参数是任务本身,一个返回一个promise的函数,下一个任务只有在这个promise解析时才会启动.第三个参数是在完成所有任务后运行的回调.如果没有传递回调,则该函数返回它创建的promise,以便我们可以处理结束.

这是一个用法示例:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );
Run Code Online (Sandbox Code Playgroud)

希望能节省一些时间......


one*_*eat 9

首先,您需要了解 Promise 在创建时执行。
例如,如果您有代码:

["a","b","c"].map(x => returnsPromise(x))
Run Code Online (Sandbox Code Playgroud)

您需要将其更改为:

["a","b","c"].map(x => () => returnsPromise(x))
Run Code Online (Sandbox Code Playgroud)

然后我们需要按顺序链接 Promise:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )
Run Code Online (Sandbox Code Playgroud)

执行after(),将确保仅在时机成熟时才创建(并执行)promise。


Mol*_*mby 6

我的首选解决方案:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}
Run Code Online (Sandbox Code Playgroud)

它与此处发布的其他内容没有根本区别,但是:

  • 将函数应用于系列项目
  • 解析为结果数组
  • 不需要异步/等待(支持仍然非常有限,大约在 2017 年)
  • 使用箭头函数;简洁明了

用法示例:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);
Run Code Online (Sandbox Code Playgroud)

在合理的当前 Chrome (v59) 和 NodeJS (v8.1.2) 上测试。


Kev*_*chs 6

使用 ES2016 的 async/await(也许还有 ES2018 的某些功能),可以将其简化为以下形式:

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

async function readFiles(files) {
  for (file in files) {
     await readFile(file)
  }
}
Run Code Online (Sandbox Code Playgroud)

我还没有看到另一个答案表达了这种简单性。OP 表示不需要并行执行 readFile。然而,对于这样的 IO,在保持循环执行同步的同时,不阻塞单个文件的读取确实很有意义(在读取所有文件之前,您不想执行下一步)。由于我刚刚了解到这一点并且对此感到有点兴奋,因此我将分享 readFile 的并行异步执行与 readFiles 的整体同步执行的方法。

async function readFiles(files) {
  await Promise.all(files.map(readFile))
}
Run Code Online (Sandbox Code Playgroud)

这难道不是一件美事吗?


Mik*_*stö 5

我能想到的最好的解决方案是使用bluebird承诺。您可以Promise.resolve(files).each(fs.readFileAsync);保证按顺序依次解决承诺。


Hai*_*han 5

这是上面另一个答案的轻微变化。使用原生 Promise:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}
Run Code Online (Sandbox Code Playgroud)

解释

如果你有这些任务[t1, t2, t3],那么上面就相当于Promise.resolve().then(t1).then(t2).then(t3)。这是reduce的行为。

如何使用

首先你需要构建一个任务列表!任务是不接受任何参数的函数。如果您需要将参数传递给您的函数,则使用bind或 其他方法来创建任务。例如:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)
Run Code Online (Sandbox Code Playgroud)