如何在.then()链中访问以前的承诺结果?

我已经将我的代码重组为承诺 ,并构build了一个美妙的长平诺言链 ,由多个.then()callback组成。 最后,我想返回一些复合值,并需要访问多个中间承诺结果 。 但是,从序列中间的分辨率值不在最后一次callback的范围内,我该如何访问它们?

 function getExample() { return promiseA(…).then(function(resultA) { // Some processing return promiseB(…); }).then(function(resultB) { // More processing return // How do I gain access to resultA here? }); } 

打破链条

当您需要访问链条中的中间值时,您应该将链条拆分成您需要的单件。 而不是附加一个callback,并尝试多次使用它的参数,附加多个callback相同的承诺 – 无论你需要的结果值。 不要忘记,一个承诺只代表(代理)未来的价值 ! 接下来从线性链中的另一个承诺中获得一个承诺,使用由库提供​​给您的承诺组合器来构build结果值。

这将导致一个非常简单的控制stream程,function清晰的组成,因此容易模块化。

 function getExample() { var a = promiseA(…); var b = a.then(function(resultA) { // some processing return promiseB(…); }); return Promise.all([a, b]).then(function([resultA, resultB]) { // more processing return // something using both resultA and resultB }); } 

而不是在Promise.all之后的callback中的参数解构,只有在ES6中可用,在ES5中, then调用将被许多诺言库( Q , Bluebird , when ,…)提供的漂亮的辅助方法取代.spread(function(resultA, resultB) { …

Bluebird还具有一个专门的join函数 ,用一个更简单(更高效)的构造来代替Promise.all + spread组合:

 … return Promise.join(a, b, function(resultA, resultB) { … }); 

ECMAScript和谐

当然,这个问题也得到了语言devise者的认可。 他们做了很多工作, asynchronousfunction提案最终成功

ECMAScript 8

你不需要一个单独的调用或callback函数,就像在一个asynchronous函数中(当被调用时返回一个promise),你可以直接等待promise直接parsing。 它还具有任意的控制结构,如条件,循环和try-catch-clause,但为了方便起见,我们不需要在这里:

 async function getExample() { var resultA = await promiseA(…); // some processing var resultB = await promiseB(…); // more processing return // something using both resultA and resultB } 

ECMAScript 6

当我们等待ES8的时候,我们已经使用了类似的语法。 ES6提供了生成器函数 ,可以将执行分割为任意放置的yield关键字。 这些切片可以独立地,甚至asynchronous地一个接一个地运行 – 而这正是我们在运行下一步之前等待承诺解决scheme时所做的。

有专门的库(如co或task.js ),但许多承诺库都有辅助函数( Q , Bluebird , when ,…),当你为它们提供一个生成器函数时,为你做这个asynchronous的分步执行产生承诺。

 var getExample = Promise.coroutine(function* () { // ^^^^^^^^^^^^^^^^^ Bluebird syntax var resultA = yield promiseA(…); // some processing var resultB = yield promiseB(…); // more processing return // something using both resultA and resultB }); 

自从4.0版本开始,Node.js就可以工作,而且一些浏览器(或者它们的开发版本)也相对较早地支持生成器语法。

ECMAScript 5

但是,如果您想要/需要向后兼容,则不能使用没有转译器的那些。 当前的工具支持生成器函数和asynchronous函数,例如参见生成器和asynchronous函数上的Babel文档。

然后,还有许多其他的编译到JS的语言 ,致力于缓解asynchronous编程。 它们通常使用类似于await的语法(例如Iced CoffeeScript ),但是也有其他类似于Haskelltypes的注释(例如, LatteJs , monadic , PureScript或LispyScript )。

同步检查

为variables分配promise-for-later-needed-values,然后通过同步检查获取它们的值。 该示例使用bluebird的.value()方法,但许多库提供了类似的方法。

 function getExample() { var a = promiseA(…); return a.then(function() { // some processing return promiseB(…); }).then(function(resultB) { // a is guaranteed to be fulfilled here so we can just retrieve its // value synchronously var aValue = a.value(); }); } 

这可以用于尽可能多的值,只要你喜欢:

 function getExample() { var a = promiseA(…); var b = a.then(function() { return promiseB(…) }); var c = b.then(function() { return promiseC(…); }); var d = c.then(function() { return promiseD(…); }); return d.then(function() { return a.value() + b.value() + c.value() + d.value(); }); } 

嵌套(和)closures

使用闭包来维护variables的范围(在我们的例子中,成功的callback函数参数)是自然的JavaScript解决scheme。 有了承诺,我们可以任意地嵌套和压扁。然后 .then()callback – 它们在语义上是等价的,除了内部范围之外。

 function getExample() { return promiseA(…).then(function(resultA) { // some processing return promiseB(…).then(function(resultB) { // more processing return // something using both resultA and resultB; }); }); } 

当然,这是build立一个缩进金字塔。 如果缩进量过大,您仍然可以使用旧的工具来对付末日金字塔 :modularize,使用额外的命名函数,并在您不再需要variables的时候立即展平promise链。
从理论上讲,你总是可以避免两层以上的嵌套(通过明确所有closures),在实践中尽可能多地使用合理的嵌套。

 function getExample() { // preprocessing return promiseA(…).then(makeAhandler(…)); } function makeAhandler(…) return function(resultA) { // some processing return promiseB(…).then(makeBhandler(resultA, …)); }; } function makeBhandler(resultA, …) { return function(resultB) { // more processing return // anything that uses the variables in scope }; } 

你也可以为这种部分应用程序使用助手函数,比如从Underscore / lodash的_.partial或本地的.bind()方法 ,以进一步减less缩进:

 function getExample() { // preprocessing return promiseA(…).then(handlerA); } function handlerA(resultA) { // some processing return promiseB(…).then(handlerB.bind(null, resultA)); } function handlerB(resultA, resultB) { // more processing return // anything that uses resultA and resultB } 

显式传递

类似于嵌套callback,这种技术依赖于闭包。 然而,链条保持不变 – 而不是只传递最新的结果,每个步骤都会传递一些状态对象。 这些状态对象累积先前操作的结果,将所有稍后需要的值加上当前任务的结果。

 function getExample() { return promiseA(…).then(function(resultA) { // some processing return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] } }).then(function([resultA, resultB]) { // more processing return // something using both resultA and resultB }); } 

在这里,那个小箭头b => [resultA, b]是closuresresultA的函数,并将两个结果的数组传递给下一个步骤。 它使用参数解构语法再次将其分解为单个variables。

在ES6解构之前,许多promise库( Q , Bluebird , when ,…)提供了一个叫做.spread()的漂亮助手方法。 它使用一个带有多个参数的函数 – 每个数组元素一个 – 用作.spread(function(resultA, resultB) { …

当然,这里需要的closures可以通过一些帮助函数进一步简化,例如

 function addTo(x) { // imagine complex `arguments` fiddling or anything that helps usability // but you get the idea with this simple one: return res => [x, res]; } … return promiseB(…).then(addTo(resultA)); 

或者,你可以使用Promise.all来产生数组的承诺:

 function getExample() { return promiseA(…).then(function(resultA) { // some processing return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped // as if passed to Promise.resolve() }).then(function([resultA, resultB]) { // more processing return // something using both resultA and resultB }); } 

你不仅可以使用数组,而且可以使用任意复杂的对象。 例如,在另一个辅助函数中使用_.extendObject.assign

 function augment(obj, name) { return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; }; } function getExample() { return promiseA(…).then(function(resultA) { // some processing return promiseB(…).then(augment({resultA}, "resultB")); }).then(function(obj) { // more processing return // something using both obj.resultA and obj.resultB }); } 

虽然这种模式保证了一个平坦的链条,而明确的状态对象可以提高清晰度,但对于长链来说,这将变得乏味。 尤其是当你只是零星地需要这个状态的时候,你还是要经过每一步。 有了这个固定的接口,链中的单个callback变得相当紧密和不灵活。 它使得更难分解单个步骤,并且callback不能直接从其他模块中提供 – 它们总是需要被封装在关注状态的样板代码中。 像上面这样的抽象帮助函数可以缓解一点痛苦,但是它总是存在的。

可变的上下文状态

微不足道的(但不合理的,相当错误的)解决scheme是只使用更高范围的variables(链中的所有callback都可以访问),并在得到结果值时写入结果值:

 function getExample() { var resultA; return promiseA(…).then(function(_resultA) { resultA = _resultA; // some processing return promiseB(…); }).then(function(resultB) { // more processing return // something using both resultA and resultB }); } 

也可以使用一个(最初是空的)对象,而不是多个variables,结果作为dynamic创build的属性存储在该对象上。

这个解决scheme有几个缺点:

  • 可变状态是丑陋的 , 全局variables是邪恶的 。
  • 这种模式不能跨越function边界,模块化function比较困难,因为它们的声明不能离开共享范围
  • variables的范围并不妨碍在初始化之前访问它们。 对于可能发生竞态条件的复杂承诺结构(循环,分支,可选),这尤其可能。 明确地传递状态,一个承诺鼓励的声明式devise ,强制一个更清晰的编码风格,可以防止这种情况。
  • 必须正确select那些共享variables的范围。 对于执行的函数,它需要是局部的,以防止多个并行调用之间的竞争条件,例如,如果状态被存储在实例上。

Bluebird库鼓励使用传递的对象,使用bind()方法将上下文对象分配给承诺链。 它将通过其他不可用的关键字从每个callback函数访问。 尽pipe对象属性比variables更容易被检测到错字,但是模式非常巧妙:

 function getExample() { return promiseA(…) .bind({}) // Bluebird only! .then(function(resultA) { this.resultA = resultA; // some processing return promiseB(…); }).then(function(resultB) { // more processing return // something using both this.resultA and resultB }).bind(); // don't forget to unbind the object if you don't want the // caller to access it } 

在不支持.bind的承诺库中,可以很容易地模拟这种方法(尽pipe以一种更详细的方式,不能在expression式中使用):

 function getExample() { var ctx = {}; return promiseA(…) .then(function(resultA) { this.resultA = resultA; // some processing return promiseB(…); }.bind(ctx)).then(function(resultB) { // more processing return // something using both this.resultA and resultB }.bind(ctx)); } 

另一个答案,使用babel-node版本<6

使用async - await

npm install -g babel@5.6.14

example.js:

 async function getExample(){ let response = await returnPromise(); let response2 = await returnPromise2(); console.log(response, response2) } getExample() 

然后,运行babel-node example.js和瞧!

节点7.4现在支持带有和声标志的asynchronous/等待呼叫。

尝试这个:

 async function getExample(){ let response = await returnPromise(); let response2 = await returnPromise2(); console.log(response, response2) } getExample() 

并运行该文件:

node --harmony-async-await getExample.js

尽可能简单!

另一个答案是,使用顺序执行程序nsynjs :

 function getExample(){ var response1 = returnPromise1().data; // promise1 is resolved at this point, '.data' has the result from resolve(result) var response2 = returnPromise2().data; // promise2 is resolved at this point, '.data' has the result from resolve(result) console.log(response, response2); } nynjs.run(getExample,{},function(){ console.log('all done'); }) 

更新:添加工作示例

 function synchronousCode() { var urls=[ "https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js", "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js", "https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" ]; for(var i=0; i<urls.length; i++) { var len=window.fetch(urls[i]).data.text().data.length; // ^ ^ // | +- 2-nd promise result // | assigned to 'data' // | // +-- 1-st promise result assigned to 'data' // console.log('URL #'+i+' : '+urls[i]+", length: "+len); } } nsynjs.run(synchronousCode,{},function(){ console.log('all done'); }) 
 <script src="amaksr/nsynjs/master/nsynjs.js"></script> 

我不会在我自己的代码中使用这个模式,因为我不是使用全局variables的忠实粉丝。 然而,在一个捏它会起作用。

用户是一个promisifiedmongoose模型。

 var globalVar = ''; User.findAsync({}).then(function(users){ globalVar = users; }).then(function(){ console.log(globalVar); }); 

在使用蓝鸟时,可以使用.bind方法在promise链中共享variables:

 somethingAsync().bind({}) .spread(function (aValue, bValue) { this.aValue = aValue; this.bValue = bValue; return somethingElseAsync(aValue, bValue); }) .then(function (cValue) { return this.aValue + this.bValue + cValue; }); 

请查看以下链接了解更多信息:

http://bluebirdjs.com/docs/api/promise.bind.html

 function getExample() { var retA, retB; return promiseA(…).then(function(resultA) { retA = resultA; // Some processing return promiseB(…); }).then(function(resultB) { // More processing //retA is value of promiseA return // How do I gain access to resultA here? }); } 

简单的方法:D

我想你可以使用RSVP的散列。

如下所示:

  const mainPromise = () => { const promise1 = new Promise((resolve, reject) => { setTimeout(() => { console.log('first promise is completed'); resolve({data: '123'}); }, 2000); }); const promise2 = new Promise((resolve, reject) => { setTimeout(() => { console.log('second promise is completed'); resolve({data: '456'}); }, 2000); }); return new RSVP.hash({ prom1: promise1, prom2: promise2 }); }; mainPromise() .then(data => { console.log(data.prom1); console.log(data.prom2); }); 

“可变语境状态”

使用本地范围的对象来收集承诺链中的中间结果是您提出的问题的合理方法。 考虑下面的代码片段:

 function getExample(){ //locally scoped const results = {}; return promiseA(...).then(function(resultA){ results.a = resultA; return promiseB(...); }).then(function(resultB){ results.b = resultB; return promiseC(...); }).then(function(resultC){ //Resolve with composite of all promises return Promise.resolve(results.a + results.b + resultC); }).catch(function(error){ return Promise.reject(error); }); } 
  • 全局variables是不好的,所以这个解决scheme使用了一个局部范围的variables,不会造成任何损害 只有在封闭的承诺内才能访问。
  • 可变的状态是丑陋的,但这不会以丑陋的方式改变状态。 丑陋的可变状态传统上是指修改函数参数或全局variables的状态,但是这种方法只是简单地修改局部范围variables的状态,这个variables的存在只是为了汇总promise结果……一个简单的死亡variables一旦承诺解决。
  • 中间承诺不会阻止访问结果对象的状态,但是这并不会导致一些可怕的情况,即链中的某个承诺会stream氓并破坏您的结果。 在承诺的每个步骤中设置值的责任仅限于此function,整体结果可能是正确的或不正确的……它不会有一些错误会在数年后出现在生产中(除非您打算!)
  • 这不会引入由并行调用引起的争用情况,因为每次调用getExample函数都会创build一个新的结果variables实例。

这一天,我也遇到了像你这样的问题。 最后,我find了一个很好的解决方法,简单而好读。 我希望这可以帮助你。

根据how-to-chain-javascript-promises

好的,让我们来看看代码:

 const firstPromise = () => { const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log('first promise is completed'); resolve({data: '123'}); }, 2000); }); }; const secondPromise = (someStuff) => { const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log('second promise is completed'); resolve({newData: `${someStuff.data} some more data`}); }, 2000); }); }; const thirdPromise = (someStuff) => { const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log('third promise is completed'); resolve({result: someStuff}); }, 2000); }); }; firstPromise() .then(seondPromise) .then(thirdPromise) .then(data => { console.log(data); });