如何从Promise中正确返回多个值?

117

最近我遇到了几次一个特定的情况,我不知道如何正确地解决。假设以下代码:

somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )
  
function afterSomething( amazingData ) {
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
}

现在可能会出现这样一种情况,我希望在afterSomethingElse中访问amazingData

一个显而易见的解决方案是从afterSomething返回一个数组或哈希表,因为你只能从函数中返回一个值。但我想知道是否有一种方法可以使afterSomethingElse接受两个参数并以相同的方式调用它,因为这似乎更容易记录和理解。

我只是想知道这种可能性是否存在,因为有Q.spread,它做了我想要的类似的事情。


1
可能是Can promises have multiple arguments to onFulfilled?的重复问题。 - Hirurg103
ES6中的解构赋值会有所帮助。请查看此处 - Ravi Teja
9个回答

108

你不能像从函数中返回多个值一样,使用多个属性来解决一个 Promise。Promise 的概念在概念上代表了一个随时间变化的值,因此虽然你可以表示复合值,但你不能将多个值放入一个 Promise 中。

一个 Promise 本质上是解析为单个值 - 这是 Q 的工作方式的一部分,Promises/A+ 规范的工作方式以及 这种抽象 的工作方式。

你可以通过使用 Q.spread 并返回数组或者使用 ES6 解构(如果支持)或者使用类似 BabelJS 这样的转译工具来实现最接近的效果。

至于如何将上下文传递到 Promise 链,请参考 Bergi 的优秀指南


28
用具有多个属性的对象来解决问题有什么问题?看起来这是一种简单的方法,可以同时获取多个值。 - jfriend00
6
可以这样做,没问题。 - Benjamin Gruenbaum
你也可以扩展 Promise,像 Bluebird 一样拥有 .spread() 方法,就像这个相关的回答中所展示的:https://dev59.com/TmEh5IYBdhLWcg3wIgYX#22776850 - Kevin Ghadyani
Promise.all()的行为似乎与此相矛盾。在所有现代JavaScript引擎中,Promise.all([a, b, c]).then(function(x, y, z) {...})可以正确工作,其中x、y和z将评估为a、b和c的解析值。因此更准确地说,语言不允许您轻松(或理智地)从用户代码中执行此操作(因为您可以直接从then子句返回一个Promise,您可以将您的值包装在promises中,然后再使用Promise.all()包装这些值以获得所需的行为,尽管方式有些复杂)。 - Austin Hemmelgarn
8
@AustinHemmelgarn 这是错误的,Promise.all会用一个数组来完成fulfillment。在 Promise.all([a,b]).then((a, b) => 中,bundefined。这就是为什么需要使用解构赋值 .then(([a, b]) => - Benjamin Gruenbaum
我会像专业人士一样将它们连接起来并拆分。 - Ralph Dingus

52

你只能传递一个值,但这个值可以是内含多个值的数组,例如:

function step1(){
  let server = "myserver.com";
  let data = "so much data, very impresive";
  return Promise.resolve([server, data]);
}

另一方面,您可以使用 ES2015 中的解构表达式来获取单个值。

function step2([server, data]){
  console.log(server); // print "myserver.com"
  console.log(data);   // print "so much data, very impresive"
  return Promise.resolve("done");
}

同时调用两个 Promise 并链式调用它们:

step1()
.then(step2)
.then((msg)=>{
  console.log(msg); // print "done"
})

5
它被称为“解构”,而不是“析构器”,它也不是一个运算符 :-/ - Bergi
3
顺便提一下,你可以在参数中直接使用解构:function step2([server, data]) { … - 这样你也避免了对隐式全局变量的赋值。而且在你的示例中,你真的应该使用 returnPromise.resolve,而不是 new Promise 构造函数。 - Bergi
感谢@Bergi的推荐! - Alejandro Silva

22

你可以返回一个包含这两个值的对象——这样做没有任何问题。

另一种策略是通过闭包保留该值,而不是将其传递:

somethingAsync().then(afterSomething);

function afterSomething(amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}

完全内联形式(等效,可以说更加一致):

somethingAsync().then(function (amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}

5
在另一个 then 语句中返回一个 then 结果是否可行?这不是一种反模式吗? - robe007
1
就像@robe007所说的那样,这不是类似于“回调地狱”吗?在这里,您的嵌套是块而不是回调函数,这将违背拥有承诺的初衷。 - some_groceries

7

只需创建一个对象并从该对象中提取参数即可。

let checkIfNumbersAddToTen = function (a, b) {
return new Promise(function (resolve, reject) {
 let c = parseInt(a)+parseInt(b);
 let promiseResolution = {
     c:c,
     d : c+c,
     x : 'RandomString'
 };
 if(c===10){
     resolve(promiseResolution);
 }else {
     reject('Not 10');
 }
});
};

从 promiseResolution 中提取参数。
checkIfNumbersAddToTen(5,5).then(function (arguments) {
console.log('c:'+arguments.c);
console.log('d:'+arguments.d);
console.log('x:'+arguments.x);
},function (failure) {
console.log(failure);
});

6

无论你从一个 Promise 返回什么,它都会被包装成一个 Promise,在下一个 .then() 阶段解封。

当你需要返回一个或多个 Promise 以及一个或多个同步值时,情况变得有趣,例如:

Promise.resolve([Promise.resolve(1), Promise.resolve(2), 3, 4])
       .then(([p1,p2,n1,n2]) => /* p1 and p2 are still promises */);

在这些情况下,使用Promise.all()来获取p1p2的承诺并在下一个.then()阶段展开是非常重要的。

Promise.resolve(Promise.all([Promise.resolve(1), Promise.resolve(2), 3, 4]))
       .then(([p1,p2,n1,n2]) => /* p1 is 1, p2 is 2, n1 is 3 and n2 is 4 */);

6

你可以做两件事,返回一个对象。

somethingAsync()
    .then( afterSomething )
    .then( afterSomethingElse );

function processAsync (amazingData) {
     //processSomething
     return {
         amazingData: amazingData, 
         processedData: processedData
     };
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}

function afterSomethingElse( dataObj ) {
    let amazingData = dataObj.amazingData,
        processedData = dataObj.proccessedData;
}

使用作用域!
var amazingData;
somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )

function afterSomething( returnedAmazingData ) {
  amazingData = returnedAmazingData;
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
  //use amazingData here
}

3

这是我认为你应该做的方式。

链的分离

因为两个函数都将使用 amazingData,所以有一个专门的函数来处理它是有意义的。 每当我想要重复使用一些数据时,我通常会这样做,以便它始终作为函数参数存在。

由于您的示例正在运行一些代码,因此我假设所有内容都在一个函数中声明。我将其称为 toto()。 然后我们将有另一个函数,它将运行 afterSomething()afterSomethingElse()

function toto() {
    return somethingAsync()
        .then( tata );
}

你还会注意到,我添加了一个return语句,因为这通常是Promise的常规用法-你总是返回一个promise,以便我们可以继续链接。在这里,somethingAsync()将生成amazingData,并且它将在新函数中的任何地方都可用。
现在,这个新函数的外观通常取决于processAsync()是否也是异步的

processAsync不是异步的

如果processAsync()不是异步的,那么没有理由过度复杂化。一些旧的好的顺序代码就可以了。
function tata( amazingData ) {
    var processed = afterSomething( amazingData );
    return afterSomethingElse( amazingData, processed );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

请注意,无论afterSomethingElse()是否执行异步操作都没有关系。如果是异步操作,则会返回一个Promise并继续链式调用。如果不是异步操作,则将返回结果值。但由于该函数是从then()中调用的,因此该值将被包装成一个Promise(至少在原始JavaScript中)。

processAsync异步

如果processAsync()是异步的,则代码将略有不同。在这里,我们假设afterSomething()afterSomethingElse()不会在其他地方重复使用。
function tata( amazingData ) {
    return afterSomething()
        .then( afterSomethingElse );

    function afterSomething( /* no args */ ) {
        return processAsync( amazingData );
    }
    function afterSomethingElse( processedData ) {
        /* amazingData can be accessed here */
    }
}

afterSomethingElse()与之前相同。它可以是异步的,也可以不是。将返回一个promise,或将值包装为已解决的promise。


您的编码风格与我以前使用的非常接近,这就是为什么我即使过了两年还回答的原因。我不太喜欢在各处使用匿名函数,我发现它很难阅读,即使在社区中很常见。这就像我们用promise-purgatory代替了callback-hell一样。
我也喜欢在then中保持函数名称简短。它们只会在本地定义。大多数情况下,它们将调用另一个已在其他地方定义的可重用函数来完成工作。甚至对于只有一个参数的函数,我也这样做,这样我就不需要在添加/删除函数签名中的参数时进出函数。
饮食示例
以下是一个示例:
function goingThroughTheEatingProcess(plenty, of, args, to, match, real, life) {
    return iAmAsync()
        .then(chew)
        .then(swallow);

        function chew(result) {
            return carefullyChewThis(plenty, of, args, "water", "piece of tooth", result);
        }

        function swallow(wine) {
            return nowIsTimeToSwallow(match, real, life, wine);
        }
}

function iAmAsync() {
    return Promise.resolve("mooooore");
}

function carefullyChewThis(plenty, of, args, and, some, more) {
    return true;
}

function nowIsTimeToSwallow(match, real, life, bobool) {
}

不要过多关注Promise.resolve(),它只是创建一个resolved promise的快捷方式。 我试图通过这种方式实现在单个位置运行所有的代码 - 就在then下面。 其他具有更具描述性名称的函数都是可重用的。
这种技术的缺点是定义了许多函数。 但是,为避免匿名函数散落在各处,这是一种必要的痛苦。 反正又有什么风险呢:堆栈溢出吗?(开玩笑!)
使用其他答案中定义的数组或对象也可以。这个答案在某种程度上就是Kevin Reid提出的答案
你还可以使用bind()Promise.all()。请注意,它们仍然需要你拆分代码。

使用bind

如果你想保持函数的可重用性,但实际上不需要将then中的内容保持非常简短,你可以使用bind()
function tata( amazingData ) {
    return afterSomething( amazingData )
        .then( afterSomethingElse.bind(null, amazingData) );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

为了简单明了,当调用函数时,bind()会将参数列表(除第一个参数外)作为首个参数添加到该函数中。

使用Promise.all

在您的帖子中,您提到了spread()的使用。我从未使用过您正在使用的框架,但是以下是您应该能够使用它的方法。
有些人认为Promise.all()是解决所有问题的方法,所以我想提一下。
function tata( amazingData ) {
    return Promise.all( [ amazingData, afterSomething( amazingData ) ] )
        .then( afterSomethingElse );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( args ) {
    var amazingData = args[0];
    var processedData = args[1];
}

你可以将数据传递给Promise.all() - 注意数组的存在 - 只要承诺,但确保没有承诺失败,否则它将停止处理。
而且,您应该能够使用spread()代替then()args参数定义新变量以进行各种令人惊叹的工作。

0
你可以查看由Rxjs表示的Observable,它允许你返回多个值

0

只需返回一个元组:

    async add(dto: TDto): Promise<TDto> {
    console.log(`${this.storeName}.add(${dto})`);
    return firebase.firestore().collection(this.dtoName)
      .withConverter<TDto>(this.converter)
      .add(dto)
      .then(d => [d.update(this.id, d.id), d.id] as [any, string])
      .then(x => this.get(x[1]));
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接