如何同步一系列的承诺?

我有一个promise对象数组,它们必须按照它们在数组中列出的顺序来parsing,也就是说,我们不能尝试parsing一个元素,直到前一个元素已经被parsing(方法all([...]) )。

如果一个元素被拒绝,我需要链条立即拒绝,而不是试图解决下面的元素。

我怎样才能实现这个,或者是否有这样的sequence模式的现有实现?

 function sequence(arr) { return new Promise(function (resolve, reject) { // try resolving all elements in 'arr', // but strictly one after another; }); } 

编辑

最初的答案build议我们只能对这些数组元素的结果进行sequence ,而不是对它们的执行,因为在这个例子中它是预定义的。

但是,那么如何以避免早期执行的方式生成一系列的promise?

这是一个修改的例子:

 function sequence(nextPromise) { // while nextPromise() creates and returns another promise, // continue resolving it; } 

我不想把它作为一个单独的问题,因为我相信这是同一个问题的一部分。

下面的一些答案和后面的讨论有点不对劲,但是最终的解决scheme正是我所期待的,它是在spex库中作为方法序列实现的 。 该方法可以迭代一系列dynamic的长度,并根据应用程序的业务逻辑的需要创buildpromise。

后来我把它变成了一个供大家使用的共享库。

下面是一些简单的例子,说明如何按顺序执行每个asynchronous操作(一个接一个地执行)。

假设你有一系列的项目:

 var arr = [...]; 

而且,您希望对数组中的每个项目执行一个特定的asynchronous操作,从而使得下一个操作不会开始,直到前一个操作完成。

而且,让我们假设你有一个promise处理函数来处理数组中的一个项目:

手动迭代

 function processItem(item) { // do async operation and process the result // return a promise } 

那么,你可以做这样的事情:

 function processArray(array, fn) { var index = 0; function next() { if (index < array.length) { fn(array[index++]).then(next); } } next(); } processArray(arr, processItem); 

手动迭代返回承诺

如果你想从processArray()返回一个promise,所以你可以知道什么时候完成,你可以添加它:

 function processArray(array, fn) { var index = 0; return new Promise(function(resolve, reject) { function next() { if (index < array.length) { fn(array[index++]).then(next, reject); } else { resolve(); } } next(); } } processArray(arr, processItem).then(function() { // all done here }, function(reason) { // rejection happened }); 

注意:这将停止第一次拒绝的链,并将该原因返回给processArray返回的承诺。

用.reduce()迭代

如果你想用承诺做更多的工作,你可以链接所有的承诺:

 function processArray(array, fn) { return array.reduce(function(p, item) { return p.then(function() { return fn(item); }); }, Promise.resolve()); } processArray(arr, processItem).then(function(result) { // all done here }, function(reason) { // rejection happened }); 

注意:这会在第一次拒绝时停止链,并将该原因传递回从processArray()返回的promise。

对于成功的场景,从processArray()返回的promise将会用你最后parsing的fncallback的值来parsing。 如果你想积累一个结果列表并且用这个结果来解决,你可以从fn一个闭包数组中收集结果,并且每次都继续返回这个数组,所以最终的结果将是一个结果数组。

用数组解决的.reduce()迭代

而且,由于现在看起来很明显,您希望最终的承诺结果是一个数据数组(按顺序),下面是对以前的解决scheme进行的修订:

 function processArray(array, fn) { var results = []; return array.reduce(function(p, item) { return p.then(function() { return fn(item).then(function(data) { results.push(data); return results; }); }); }, Promise.resolve()); } processArray(arr, processItem).then(function(result) { // all done here // array of data here in result }, function(reason) { // rejection happened }); 

工作演示: http : //jsfiddle.net/jfriend00/h3zaw8u8/

并显示拒绝的工作演示: http : //jsfiddle.net/jfriend00/p0ffbpoc/


仅供参考,我认为我的processArray()函数与Bluebird承诺库中的Promise.map()非常相似,它承担了一个数组和一个承诺生成函数,并返回一个parsing结果的数组。


@ vitaly-t – 这里有一些更详细的评论你的方法。 欢迎您无论看起来最好的代码。 当我第一次使用promise的时候,我倾向于只使用承诺来做最简单的事情,而在更高级地使用promise的时候,我自己写了很多逻辑,可以为我做更多的事情。 您只使用自己完全熟悉的内容,而且更希望看到自己熟悉的代码。 这可能是人性的。

我会build议,随着我越来越了解什么样的承诺可以为我所做,我现在喜欢编写使用更多承诺的先进function的代码,对我来说这似乎是非常自然的,我觉得我正在build设良好testing过的基础设施具有许多有用的function。 我只会问,当你越来越了解潜在的方向时,你就会保持开放。 我的意见是,随着您的理解的提高,迁移是一个有用的和高效的方向。

以下是您的方法的一些具体反馈意见:

你在七个地方创造承诺

作为样式的对比,我的代码只有两个地方我明确地创build了一个新的承诺 – 一次在工厂函数中,一次初始化.reduce()循环。 在其他地方,我只是build立在已经通过链接或返回值或直接返回它们而创build的承诺上。 你的代码有七个独特的地方,你正在创造一个承诺。 现在,良好的编码不是一个比赛,看看你能创造一个承诺的地方有多less,但是这可能会指出在已经创build的承诺与testing条件之间的差异,并创造新的承诺。

投掷安全是一个非常有用的function

承诺是安全的。 这意味着在promise处理程序中抛出的exception会自动拒绝这个promise。 如果你只是想让exception成为拒绝,那么这是一个非常有用的function,可以利用。 事实上,你会发现,只是抛出自己是一个有用的方式来拒绝从一个处理程序内没有创build另一个承诺。

许多Promise.resolve()Promise.reject()可能是一个简化的机会

如果你看到有很多Promise.resolve()Promise.reject()语句的代码,那么可能有机会更好地利用现有的promise,而不是创build所有这些新的promise。

投向承诺

如果你不知道是否有答复,那么你可以把它承诺。 然后,承诺库将自己检查它是否是承诺,甚至是否与您使用的承诺库相匹配,如果不是,则将其封装到一个承诺库中。 这可以节省你自己重写这个逻辑。

合同返还承诺

在现在的很多情况下,为一个可能做一些asynchronous操作来返回一个承诺的函数签订合同是完全可行的。 如果函数只是想做一些同步的事情,那么它可以返回一个已经解决的承诺。 你似乎觉得这很麻烦,但这绝对是风的方式,而且我已经写了很多需要的代码,而且一旦熟悉了承诺就会感觉很自然。 它抽象出操作是同步还是asynchronous,调用者不必知道或者做任何特殊的事情。 这是一个很好的使用承诺。

工厂function可以写成只能创build一个承诺

工厂函数可以写成只创build一个承诺,然后解决或拒绝它。 这种风格也可以保证安全,因此在工厂function中发生的任何exception都会自动成为拒绝。 它也使合同总是自动返回一个承诺。

虽然我意识到这个工厂函数是一个占位符函数(它甚至不做任何asynchronous),希望你可以看到风格考虑它:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve("one"); break; case 1: resolve("two"); break; case 2: resolve("three"); break; default: resolve(null); break; } }); } 

如果这些操作中的任何一个都是asynchronous的,那么他们可以只是返回自己的承诺,这将自动链接到这样一个中心承诺:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve($.ajax(...)); case 1: resole($.ajax(...)); case 2: resolve("two"); break; default: resolve(null); break; } }); } 

使用拒绝处理程序来return promise.reject(reason)是不需要的

当你有这样的代码体系:

  return obj.then(function (data) { result.push(data); return loop(++idx, result); }, function (reason) { return promise.reject(reason); }); 

拒绝处理程序不添加任何值。 你可以改为这样做:

  return obj.then(function (data) { result.push(data); return loop(++idx, result); }); 

您已经返回obj.then()的结果。 如果obj拒绝,或者如果有任何链接到obj或从那里返回的.then()处理程序拒绝,则obj将拒绝。 所以你不需要用拒绝来创build一个新的承诺。 没有拒绝处理程序的简单代码用较less的代码执行相同的操作。


以下是您的代码的一般架构中的一个版本,它试图整合大部分这些想法:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve("zero"); break; case 1: resolve("one"); break; case 2: resolve("two"); break; default: // stop further processing resolve(null); break; } }); } // Sequentially resolves dynamic promises returned by a factory; function sequence(factory) { function loop(idx, result) { return Promise.resolve(factory(idx)).then(function(val) { // if resolved value is not null, then store result and keep going if (val !== null) { result.push(val); // return promise from next call to loop() which will automatically chain return loop(++idx, result); } else { // if we got null, then we're done so return results return result; } }); } return loop(0, []); } sequence(factory).then(function(results) { log("results: ", results); }, function(reason) { log("rejected: ", reason); }); 

工作演示: http : //jsfiddle.net/jfriend00/h3zaw8u8/

关于这个实现的一些评论:

  1. Promise.resolve(factory(idx))基本上将Promise.resolve(factory(idx))的结果转换为承诺。 如果它只是一个价值,那么它就成为一个解决的承诺,以这个回报价值作为解决价值。 如果这已经是一个承诺,那么它只是链接到这个承诺。 因此,它会replacefactory()函数返回值上的所有types检查代码。

  2. 工厂函数表示它是通过返回null或parsing值最终为null的承诺来完成的。 上面的cast将这两个条件映射到相同的结果代码。

  3. 工厂函数会自动捕获exception,并将它们转换为不合格,然后由sequence()函数自动处理。 如果您只是想中止处理并将错误反馈到第一个exception或拒绝,那么让承诺处理大量error handling是一个显着的优势。

  4. 在这个实现中的工厂函数可以返回一个promise或一个静态值(用于一个同步操作),并且它可以正常工作(根据你的devise请求)。

  5. 我已经在工厂函数的promisecallback中抛出exception进行了testing,它确实只是拒绝并传播该exception,以拒绝序列承诺,例外情况为理由。

  6. 这使用了一个类似的方法(试图保留你的通用架构)来链接多个调用loop()

承诺代表操作的价值 ,而不是操作本身。 操作已经开始,所以你不能让他们彼此等待。

相反,您可以同步函数,以便按顺序(通过带有承诺链的循环)或使用蓝鸟中的.each方法来返回调用它们的.each

您不能简单地运行Xasynchronous操作,然后希望按顺序解决它们。

正确的做这种事情的方法是,只有在解决之前,才能运行新的asynchronous操作:

 doSomethingAsync().then(function(){ doSomethingAsync2().then(function(){ doSomethingAsync3(); ....... }); }); 

编辑
似乎你想等待所有的承诺,然后以特定的顺序调用callback。 像这样的东西:

 var callbackArr = []; var promiseArr = []; promiseArr.push(doSomethingAsync()); callbackArr.push(doSomethingAsyncCallback); promiseArr.push(doSomethingAsync1()); callbackArr.push(doSomethingAsync1Callback); ......... promiseArr.push(doSomethingAsyncN()); callbackArr.push(doSomethingAsyncNCallback); 

接着:

 $.when(promiseArr).done(function(promise){ while(callbackArr.length > 0) { callbackArr.pop()(promise); } }); 

可能发生的问题是当一个或多个承诺失败时。

虽然相当密集,但这里还有另外一个解决scheme,它将对一个数组值进行promise-returns函数的迭代,并用一组结果来解决:

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

用法:

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

请注意,因为我们正在从一个承诺减less到下一个,每个项目都被串联处理。

它在function上等同于“使用数组解决的.reduce()与@ jfriend00解决scheme的迭代”,但有点整洁。