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


269

考虑以下以串行/顺序方式读取文件数组的代码。readFiles返回一个promise,仅当依次读取所有文件后,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!

  });
};

上面的代码可以工作,但是我不喜欢必须递归使事情顺序发生。有没有更简单的方法可以重写此代码,这样我就不必使用怪异的代码了readSequential函数了?

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

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

2
必须等待上一个异步操作完成的所有操作都必须在回调中完成。使用诺言并不会改变这一点。因此,您需要递归。
巴尔2014年

1
仅供参考,从技术上讲,这不是递归,因为没有堆栈框架建立。前面readFileSequential()已经返回下一个被调用之前(因为它的异步,它完成后,长原函数调用已经返回)。
jfriend00 2014年

1
@ jfriend00递归不需要堆栈帧累积-仅用于自参考。不过,这只是技术问题。
本杰明·格伦鲍姆

3
@BenjaminGruenbaum-我的观点是,让函数本身进行下一次迭代完全没有错。缺点是零,实际上,这是排序异步操作的有效方法。因此,没有理由避免看起来像递归的事情。对于某些效率不高的问题,有一些递归解决方案-这不是其中之一。
jfriend00 2014年

1
嘿,根据JavaScript室中的讨论和要求,我已经编辑了此答案,因此我们可以将其他人作为规范来指出。如果您不同意,请告诉我,我将还原它并打开一个单独的文件。
本杰明·格林鲍姆

Answers:


337

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

或更紧凑地使用减少:

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

尽管今天确实没有理由使用异步等待。


2
@EmreTapcı,不。箭头函数的“ =>”已经暗示要返回。
最多

如果使用TypeScript,我认为“ for in”循环解决方案是最好的。减少收益递归的承诺,例如。第一个呼叫返回类型是Promise <void>,然后第二个是Promise <Promise <void >>,依此类推-如果不使用我认为的任何类型,都无法键入
Artur Tagisow

@ArturTagisow TypeScript(至少是新版本)具有递归类型,在此处正确解析这些类型。因为Promise“递归吸收”,所以没有Promise <Promise <T >>这样的东西。Promise.resolve(Promise.resolve(15))与相同Promise.resolve(15)
本杰明·格林鲍姆


72

这是我更喜欢连续运行任务的方式。

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

有更多任务的情况又如何呢?像十岁吗?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
如果您不知道确切的任务数量,该怎么办?
该死

1
当您知道任务的数量而仅在运行时知道该怎么办?
joeytwiddle

10
“您根本根本不想对一组promise进行操作。根据promise规范,一旦创建promise,它就会开始执行。因此,您真正想要的是一组promise工厂”,请参阅高级错误#3此处:pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans,2016年

5
如果您想减少线路噪音,也可以写result = result.then(task);
Daniel Buckmaster,

1
@DanielBuckmaster是的,但是要小心,因为如果task()返回一个值,它将被传递给下一个调用。如果您的任务具有可选参数,则可能会产生副作用。当前代码吞噬了结果,并且不带任何参数地显式调用下一个任务。
JHH

63

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

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

相反,我们需要创建一个返回诺言的函数数组。然后将依次执行每个函数,然后在内部启动Promise。

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

reduce与promise结合使用时会有些棘手,因此我将一根衬里分解成下面一些较小的易消化的小块。

此函数的本质是使用reducePromise.resolve([]),或包含空数组的诺言开头的初始值。

然后,此承诺将作为传递到reduce方法中promise。这是将每个承诺按顺序链接在一起的关键。下一个要执行的promise是func,当then触发时,将结果连接起来,然后返回该promise,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([]))

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

1
很好,谢谢,Array.prototype.concat.bind(result)是我所缺少的部分,必须手动推送结果,该方法虽然有效,但效果却不那么好
zavr

既然我们都是关于现代JS的,那么我相信console.log.bind(console)您现在在最后一个示例中的语句现在通常是不必要的。这些天你可以过去console.log。例如。serial(funcs).then(console.log)。在当前的nodejs和Chrome上进行了测试。
Molomby

这有点难缠我的头,但减少本质上是做对了吗? 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)); });
danecando

@danecando,是的,这看起来是正确的。您也可以在返回中删除Promise.resolve,除非您对它们调用Promise.reject,否则返回的任何值都将自动解析。
joelnet

@joelnet,在回应danecando的评论时,我认为reduce在以下表达式中应该更正确地表达,您是否同意?Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))等等
bufferoverflow76

37

要在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;
}

1
似乎正在使用下划线。您可以简化为files.forEach文件是否为数组。
古斯塔沃·罗德里格斯

2
嗯...是ES5。ES6的方式是for (file of files) {...}
古斯塔沃·罗德里格斯

1
您说您不应该Promise.resolve()用来在现实生活中创建已经解决的承诺。为什么不?Promise.resolve()似乎比new Promise(success => success())
canac

8
@canac抱歉,这只是一个玩笑,带有文字(“空虚的承诺..”)。绝对Promise.resolve();在您的代码中使用。
Shridhar Gupta

1
不错的解决方案,易于遵循。我没有将我的函数包含在函数中,因此请在最后解决而不是放return sequence;我自己sequence.then(() => { do stuff });
Joe Coyle

25

标准Node.js的简单util承诺:

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

更新

items-promise是准备使用的NPM软件包。


6
我希望看到更详细的解释。
Tyguy17年

我在下面的解释中提供了该答案的变体。谢谢
Sarsaparilla

这正是我在节点7之前的环境中无法访问异步/等待的操作。干净整洁。
JHH

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

该函数带有2个参数+ 1个可选参数。第一个参数是我们将要处理的数组。第二个参数是任务本身,它是一个返回承诺的函数,只有在该承诺解决后,下一个任务才会启动。第三个参数是在完成所有任务后运行的回调。如果没有传递回调,则该函数返回其创建的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 );

希望它可以节省一些时间...


难以置信的解决方案,这是我在近一个星期的苦苦挣扎中找到的最好的解决方案...。它得到了很好的解释,具有合理的内部名称,一个很好的例子(可能会更好),我可以安全地呼吁它所需的时间,它包括设置回调的选项。简直好极了!(只需将名称更改为更有意义的名称即可)。...其他建议...您可以使用'Object.keys(myObject)'作为'objects_array' 迭代对象
DavidTaubmann

谢谢你的评论!我也没有使用该名称,但是我想在这里使其更明显/更简单。
Salketer '17

5

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


1
甚至更好:Promise.each(filtes, fs.readFileAsync)。顺便说一句,你不必做.bind(fs)吗?
Bergi 2015年

这里似乎没有人理解数组和序列之间的区别,后者意味着无限/动态的大小。
vitaly-t 2015年

请注意,Java语言中的数组与C样式语言中的固定大小的数组无关。它们只是附加了数字键管理的对象,没有规定的大小或限制(尤其是在使用时不是如此)new Array(int)。所有这些操作都是预置length键-值对,影响在基于长度的迭代过程中使用了多少个索引。它为零对实际数组的索引或索引范围的影响)
Mike'Pomax'Kamermans

4

这是上面另一个答案的细微变化。使用本机承诺:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

说明

如果您有这些任务[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(...)

4

我的首选解决方案:

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

它与此处发布的其他文档没有根本区别,但:

  • 将功能套用至系列项目
  • 解析为一系列结果
  • 不需要异步/等待(支持仍然非常有限,大约在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);

在当前最新的Chrome(v59)和NodeJS(v8.1.2)上进行了测试。


3

使用Array.prototype.reduce,并记住将您的Promise包装在一个函数中,否则它们将已经运行!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

简单易用...您应该能够重复使用同一种子来提高性能,等等。

使用reduce时防止空数组或只有1个元素的数组很重要,因此,此技术是您最好的选择:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

然后将其命名为:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

我在Promise对象上创建了以下简单方法:

创建Promise.sequence方法并将其添加到Promise对象

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

用法:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

对此Promise对象的扩展最好的一点是,它与Promise的样式一致。Promise.all和Promise.sequence的调用方式相同,但语义不同。

警告

顺序运行promise通常不是使用promise的好方法。通常最好使用Promise.all,并让浏览器尽可能快地运行代码。但是,它确实有用例-例如,当使用javascript编写移动应用程序时。


不,您无法与Promise.all和进行比较Promise.sequence。一个确实采取了可迭代的诺言,另一个采取了一系列返回诺言的函数。
Bergi 2015年

顺便说一句,我建议避免使用promise构造器反模式
Bergi

不知道它需要一个迭代器。应该很容易重写它。您能否详细说明为什么这是promise构造函数的反模式?我确实在这里阅读了您的文章:stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi我已经更新了代码以支持迭代器。我仍然看不到这是一种反模式。通常将反模式视为避免编码错误的准则,并且创建破坏这些准则的(库)函数是完全有效的。
frodeborli

是的,如果您认为它是一种库函数,那是可以的,但是在这种情况下reduce,本杰明的答案中的相似之处要简单得多。
Bergi

2

您可以使用此功能获取promiseFactories列表:

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Promise Factory是返回Promise的简单函数:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

之所以有效,是因为Promise工厂直到被要求时才创建Promise。它与then函数的工作方式相同–实际上,这是同一件事!

您根本不希望兑现一系列承诺。根据Promise规范,一旦创建了承诺,它就会开始执行。所以您真正想要的是一系列承诺工厂...

如果您想了解有关Promises的更多信息,则应查看以下链接:https : //pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

我的回答基于https://stackoverflow.com/a/31070150/7542429

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

此解决方案以类似于Promise.all()的数组形式返回结果。

用法:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

我真的很喜欢@joelnet的答案,但是对我来说,这种编码风格有点难以理解,所以我花了几天的时间试图弄清楚如何以更易读的方式表达相同的解决方案,这就是我的意思。只需使用不同的语法和一些注释即可。

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

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

正如Bergi所注意到的,我认为最好的和明确的解决方案是使用BlueBird.each,以下代码:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

首先,您需要了解在创建时已执行了诺言。
因此,例如,如果您有一个代码:

["a","b","c"].map(x => returnsPromise(x))

您需要将其更改为:

["a","b","c"].map(x => () => returnsPromise(x))

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

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

执行after(),将确保仅在承诺到来时创建(并执行)promise。


1

我使用以下代码扩展Promise对象。它处理承诺的拒绝并返回结果数组

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

如果需要,可以使用reduce来做出顺序承诺,例如:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

它将始终按顺序工作。


1

使用现代ES:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

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

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

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

  for (const file of filesList) {
    await downloadFile(file);
  }
}

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

没有异步/等待(使用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);
}

2
不建议在forEach内部等待。
MarceloAgimóvel19年

@MarceloAgimóvel -我已经更新,以解决不工作,forEach(根据
吉尔Epshtain

0

根据问题标题“解决一个接一个的承诺(即按顺序)?”,我们可能会理解,与按顺序调用本身相比,OP对结算中的按顺序处理承诺更感兴趣。

提供此答案:

  • 证明顺序调用对于顺序处理响应不是必需的。
  • 向该页面的访问者展示可行的替代模式-包括OP,如果他在一年后仍对此感兴趣。
  • 尽管OP声称他不想同时进行呼叫,这确实是真实的情况,但同样可能是基于对标题隐含的对响应进行顺序处理的期望的假设。

如果确实不希望进行并发呼叫,则请参阅本杰明·格伦鲍姆的答案,其中全面涵盖了顺序呼叫(等)。

但是,如果您对允许并发调用并依次处理响应的模式感兴趣(以提高性能),请继续阅读。

人们很容易认为你必须使用Promise.all(arr.map(fn)).then(fn)(因为我已经做过很多次)或无极LIB的幻想糖(尤其是蓝鸟的),但是(以信誉这篇文章)的arr.map(fn).reduce(fn)模式将做的工作,与优点,即:

  • 只能与任何promise库一起使用-甚至包括jQuery的预兼容版本.then()
  • 可以灵活地跳过错误或错误停止,无论您希望使用哪种模式都可以。

这是为编写的Q

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

注意:只有一个片段Q()Q特定于Q。对于jQuery,您需要确保readFile()返回jQuery Promise。有了A +库,外国的承诺就会被吸收。

这里的关键是归约的sequence诺言,该诺言对诺言的处理进行排序,readFile而不是对诺言的创建进行排序。

一旦您了解了这一点,当您意识到该.map()阶段实际上不是必需的时,可能会有点令人费解!整个工作,并行调用以及按正确顺序进行的串行处理都可以reduce()单独完成,此外还具有以下优点:

  • 只需移动一条线即可将并行异步调用转换为串行异步调用-在开发过程中可能很有用。

这是为Q一次。

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

这是基本模式。如果您还想将数据(例如文件或文件的某种转换)传递给调用者,则需要一个温和的变体。


我认为回答违反OP意图的问题不是一个好主意……
Bergi 2015年

1
这个sequence.then(() => filePromise)东西是一种反模式-它不会尽快传播错误(并unhandledRejection在支持错误的库中创建)。您应该使用Q.all([sequence, filePromise])$.when(sequence, filePromise)。诚然,这种行为可能是您想要忽略或跳过错误时想要的,但是至少应将此作为缺点。
Bergi 2015年

@Bergi,我希望OP能够介入并就是否确实违反他的意图做出判断。如果没有,我将删除我猜的答案,同时我希望我已经证明了自己的立场。感谢您认真对待它,以提供体面的反馈。您能否进一步说明反模式,或者提供参考?我找到基本模式的文章是否也一样?
Roamer-1888

1
是的,他的代码的第三版(即“并行和顺序”)有相同的问题。“反模式”需要复杂的错误处理,并且倾向于异步附加处理程序,这会导致unhandledRejection事件。在Bluebird中,您可以通过使用sequence.return(filePromise)具有相同行为但可以很好地处理拒绝的方法来解决此问题。我不知道任何参考,我只是想出了它-我不认为“(反)模式”还没有名称。
Bergi 2015年

1
@Bergi,您可以清楚地看到一些我看不到的东西:(我想知道是否需要在某个地方记录这种新的反模式吗?
Roamer-1888

0

您的方法还不错,但是确实有两个问题:它吞没了错误,并且使用了Explicit Promise Construction Antipattern。

您可以解决这两个问题,并使代码更整洁,同时仍采用相同的常规策略:

var Q = require("q");

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

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

如果其他人在执行CRUD操作时需要一种有保证的严格的顺序方式来解决Promises,则还可以使用以下代码作为基础。

只要在调用每个函数之前添加“ return”,描述一个Promise,并以此示例为基础,下一个.then()函数调用将在上一个函数完成后始终开始:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

数组推入和弹出方法可用于承诺序列。您还可以在需要其他数据时提出新的承诺。这是代码,我将在React Infinite loader中使用它来加载页面序列。

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

大多数答案并不包括单独的ALL许诺的结果,因此,如果有人正在寻找这种特殊行为,这是使用递归的一种可能的解决方案。

它遵循以下样式Promise.all

  • 返回.then()回调中的结果数组。

  • 如果某些承诺失败,则其立即在.catch()回调中返回。

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

关于tasks数组声明的注意事项

在这种情况下,将无法使用以下符号Promise.all

const tasks = [promise(1), promise(2)]

我们必须使用:

const tasks = [() => promise(1), () => promise(2)]

原因是JavaScript在声明后立即开始执行promise。如果我们使用像这样的方法Promise.all,它只会检查所有方法的状态是否为fulfilledrejected,而不会启动执行本身。使用() => promise()我们停止执行直到被调用。


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

这里的关键是如何调用睡眠功能。您需要传递一个函数数组,该函数本身返回一个promise,而不是一个promise数组。


-1

这将扩展如何基于spex.sequence实现以更通用的方式处理承诺序列,支持动态/无限序列:

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

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

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

该解决方案不仅适用于任何大小的序列,而且您可以轻松地为其添加数据限制和负载平衡

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.