解决承诺一个接一个(即按顺序)?
考虑以下代码以串行/顺序方式读取文件数组。 readFiles
返回一个承诺,只有在所有文件被顺序读取后才会被parsing。
var Q = require("q"); var readFile = function(file) { ... // Returns a promise. }; var readFiles = function(files) { var deferred = Q.defer(); var readSequential = function(index) { if (index >= files.length) { deferred.resolve(); } else { readFile(files[index]).then(function() { readSequential(index + 1); }); } }; readSequential(0); // Start! return deferred.promise; };
上面的代码工作的代码,但我不喜欢做recursion顺序发生的事情。 有没有更简单的方法,这个代码可以重写,所以我不必使用我奇怪的readSequential
函数?
本来我试图使用Q.all
,但这导致所有的readFile
调用同时发生,这不是我想要的:
var readFiles = function(files) { return Q.all(files.map(function(file) { return readFile(file); })); };
2017年更新 :如果环境支持,我会使用asynchronous函数:
async function readFiles(files) { for(const file of files) { await readFile(file); } };
如果你愿意,你可以推迟阅读文件,直到你需要使用asynchronous生成器(如果你的环境支持它):
async function* readFiles(files) { for(const file of files) { yield await readFile(file); } };
更新:在第二个想法 – 我可能会使用一个for循环:
var readFiles = function(files) { var p = Q(); // Promise.resolve() without Q files.forEach(function(file){ p = p.then(function(){ return readFile(file); }); // or .bind }); return p; };
或者更紧凑,减less:
var readFiles = function(files) { return files.reduce(function(p, file) { return p.then(function(){ return readFile(file); }); },Q()); // initial };
在其他承诺库(如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. });
在Q中,你所拥有的就像你所能得到的一样好 – 你可以用Array.prototype.reduce
来缩短它,并将其提取到一个通用的方法中。
如果你可以使用Q.async
(你在节点上),事情会变得更好:
Q.spawn(function* () { var results = []; for(var i = 0;i < files.length; i++){ results.push(yield readFile(files[i])); } console.log(results); });
只要记住用--harmony
运行节点, 并记住它是实验性的 。
这是我更喜欢连续运行任务的方式。
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; }
这个问题很古老,但我们生活在一个ES6和function性JavaScript的世界里,所以让我们看看我们如何改进。
由于承诺立即执行,我们不能只是创build一个承诺的数组,他们都会并行地开火。
相反,我们需要创build一个返回promise的函数数组。 然后每个函数将被顺序执行,然后在里面启动promise。
我们可以通过几种方法解决这个问题,但是我最喜欢的方式是使用reduce
。
使用reduce
与承诺相结合会有点棘手,所以我已经把一行划分成了一些较小的可消化咬痕。
这个函数的实质是使用reduce
从Promise.resolve([])
的初始值开始,或者包含一个空数组的promise。
这个承诺随后将作为promise
传递给reduce
方法。 这是顺序链接每个承诺的关键。 下一个执行的承诺是func
,当then
触发时,结果被连接起来,然后返回promise,执行下一个promise函数的reduce
循环。
一旦所有的承诺都执行完毕,返回的承诺将包含每个承诺的所有结果的数组。
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))
要在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(function(file) { // Chain one computation onto the sequence sequence = sequence.then(function() { return performComputation(file); }).then(function(result) { doSomething(result) // Resolves for each file, one at a time. }); }) // This will resolve after the entire chain is resolved return sequence; }
标准Node.js的简单实用程序承诺:
function sequence(tasks, fn) { return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve()); }
UPDATE
items-promise是一个可以使用的NPM包。
我不得不运行很多顺序的任务,并使用这些答案来伪造一个函数来处理任何连续的任务。
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个可选项。 第一个参数是我们将要工作的数组。 第二个参数是任务本身,一个返回承诺的函数,只有当这个承诺解决时才会启动下一个任务。 第三个参数是所有任务完成后的callback。 如果没有callback通过,那么函数返回它创build的承诺,所以我们可以处理结束。
以下是一个使用示例:
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 );
希望能节省一些时间…
我能弄清楚的最好的解决scheme是bluebird
承诺。 你可以做Promise.resolve(files).each(fs.readFileAsync);
这保证了承诺按顺序依次解决。
我的首选解决scheme
function processArray(arr, fn) { return arr.reduce( (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))), Promise.resolve([]) ); }
这里和其他发表的文章没有根本的区别,但是:
- 将函数应用于系列中的项目
- 解决了一系列结果
- 不需要asynchronous/等待(支持仍然相当有限,大约2017年)
- 使用箭头function; 好,简洁
用法示例:
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);
testing目前的合理的Chrome(v59)和NodeJS(v8.1.2)。
我在Promise对象上创build了这个简单的方法:
创buildPromise.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.all通常会更好,并让浏览器尽可能快地运行代码。 但是,它有真实的使用情况 – 例如,使用JavaScript编写移动应用程序时。
你可以使用这个函数获取promiseFactories List:
function executeSequentially(promiseFactories) { var result = Promise.resolve(); promiseFactories.forEach(function (promiseFactory) { result = result.then(promiseFactory); }); return result; }
Promise Factory只是简单的函数,返回一个Promise:
function myPromiseFactory() { return somethingThatCreatesAPromise(); }
这是有效的,因为承诺工厂在被要求前不会创造承诺。 它和当时的function一样 – 事实上,它是一样的!
你根本不想操作一系列的promise。 根据Promise规范,只要承诺被创build,它就开始执行。 所以你真正想要的是一组承诺工厂…
如果你想了解更多的承诺,你应该检查这个链接: https : //pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
我使用下面的代码来扩展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);
如果你愿意,你可以使用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));
它将始终顺序工作。
这是上述另一个答案的轻微变化。 使用原生承诺:
function inSequence(tasks) { return tasks.reduce((p, task) => p.then(task), Promise.resolve()) }
说明
如果你有这些任务[t1, t2, t3]
,那么上面相当于Promise.resolve().then(t1).then(t2).then(t3)
。 这是减less的行为。
如何使用
首先你需要构build一个任务列表! 任务是一个不接受参数的函数。 如果你需要传递参数给你的函数,然后使用bind
或其他方法来创build一个任务。 例如:
var tasks = files.map(file => processFile.bind(null, file)) inSequence(tasks).then(...)
我真的很喜欢@ joelnet的答案,但是对于我来说,这种编码风格有点难于消化,所以我花了几天的时间想弄清楚如何以更可读的方式expression同一个解决scheme,这是我的采取不同的语法和一些意见。
// 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) })
在问题题目“依次解决承诺”的基础上,我们可以理解OP对结算承诺的顺序处理比序列化本身更感兴趣。
这个答案是提供:
- 以certificate顺序调用对顺序处理响应不是必需的。
- 为这个页面的访问者揭示可行的替代模式 – 包括OP,如果他在一年之后仍然感兴趣的话。
- 尽pipeOP声称他不想同时发出呼叫,这可能是真实的情况,但同样可能是一个基于连续处理响应的愿望的假设,正如标题所暗示的。
如果真的不需要并发呼叫,请参阅本杰明·格鲁恩鲍姆(Benjamin Gruenbaum)的全面回答呼叫(等等)的答案。
但是,如果您对允许并发呼叫,然后按顺序处理响应的模式感兴趣(为了改进性能),请继续阅读。
很有可能认为你必须使用Promise.all(arr.map(fn)).then(fn)
(正如我已经做了很多次)或者一个Promise lib的花式糖(特别是Bluebird's) )一个arr.map(fn).reduce(fn)
模式将完成这项工作,其优点是:
- 与任何promise lib一起工作 – 甚至是预先兼容的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()
所特有的。对于jQuery,您需要确保readFile()返回一个jQuery promise。 用A + libs,外国的承诺会被同化。
这里的关键是减less的sequence
承诺,顺序处理 readFile
承诺,但不是它们的创build。
一旦你吸收了这些,当你意识到.map()
阶段实际上是不必要的时候,这可能会让人有些兴奋! 并行调用和串行处理按照正确的顺序执行,可以通过reduce()
单独实现,另外还具有进一步灵活性的优点:
- 通过简单地移动一行,将并行asynchronous调用转换为串行asynchronous调用 – 在开发过程中可能有用。
这是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. }); };
这是基本模式。 如果您还想要将数据(例如文件或它们的某些转换)传送给调用者,则需要一个温和的变体。
这是为了扩展如何以更通用的方式处理一系列的promise,支持基于spex.sequence实现的dynamic/无限序列:
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; });
这个解决scheme不仅可以处理任何大小的序列,还可以轻松地为其添加数据调节和负载平衡 。
你的方法并不糟糕,但它确实有两个问题:它吞下错误,并采用显式承诺构造反模式。
您可以解决这两个问题,并使代码更清晰,同时仍采用相同的一般策略:
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! };
我的答案基于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; }); };
这个解决scheme像Promise.all()这样的数组返回结果。
用法:
Promise.series([array of promises]) .then(function(results) { // do stuff with results here });