如何同步一系列的承诺?
我有一个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的fn
callback的值来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/
关于这个实现的一些评论:
-
Promise.resolve(factory(idx))
基本上将Promise.resolve(factory(idx))
的结果转换为承诺。 如果它只是一个价值,那么它就成为一个解决的承诺,以这个回报价值作为解决价值。 如果这已经是一个承诺,那么它只是链接到这个承诺。 因此,它会replacefactory()
函数返回值上的所有types检查代码。 -
工厂函数表示它是通过返回
null
或parsing值最终为null
的承诺来完成的。 上面的cast将这两个条件映射到相同的结果代码。 -
工厂函数会自动捕获exception,并将它们转换为不合格,然后由
sequence()
函数自动处理。 如果您只是想中止处理并将错误反馈到第一个exception或拒绝,那么让承诺处理大量error handling是一个显着的优势。 -
在这个实现中的工厂函数可以返回一个promise或一个静态值(用于一个同步操作),并且它可以正常工作(根据你的devise请求)。
-
我已经在工厂函数的promisecallback中抛出exception进行了testing,它确实只是拒绝并传播该exception,以拒绝序列承诺,例外情况为理由。
-
这使用了一个类似的方法(试图保留你的通用架构)来链接多个调用
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的迭代”,但有点整洁。