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

Posted

技术标签:

【中文标题】你如何正确地从 Promise 返回多个值?【英文标题】:How do you properly return multiple values from a Promise? 【发布时间】:2015-04-26 12:54:55 【问题描述】:

我最近几次遇到某种情况,我不知道如何正确解决。假设如下代码:

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

function afterSomethingElse( processedData ) 

现在可能会出现我想在afterSomethingElse 中访问amazingData 的情况。

一个明显的解决方案是从afterSomething 返回一个数组或哈希值,因为,你只能从一个函数返回一个值。但我想知道是否有办法让 afterSomethingElse 接受 2 个参数并同样调用它,因为这似乎更容易记录和理解。

我只是想知道这种可能性,因为有 Q.spread,它的作用类似于我想要的。

【问题讨论】:

javascript promise not passing all arguments (using Q) 和 How do I access previous promise results in a .then() chain? 的可能重复项 Can promises have multiple arguments to onFulfilled?的可能重复 ES6 中的解构赋值会有所帮助。检查here 【参考方案1】:

您无法解决具有多个属性的承诺,就像您无法从函数返回多个值一样。一个 Promise 在概念上表示一个随时间变化的值,因此虽然您可以表示复合值,但您不能在一个 Promise 中放置多个值。

promise 固有地使用单个值解析 - 这是 Q 工作原理的一部分,how the Promises/A+ spec works 和 the abstraction 工作原理。

最接近的方法是使用 Q.spread 并返回数组或使用 ES6 解构(如果它受支持或您愿意使用 BabelJS 等转译工具)。

关于将上下文传递到承诺链,请参考Bergi's excellent canonical on that。

【讨论】:

解析具有多个属性的对象有什么问题?似乎是一种从解析中获取多个值的简单方法。 这样做完全没问题 您还可以扩展 Promise 以使 .spread() 像 Bluebird 一样显示在此相关答案中:***.com/a/22776850/1624862 Promise.all() 的行为似乎与此相矛盾。 Promise.all([a, b, c]).then(function(x, y, z) ...) 在所有现代 Javascript 引擎中都能正常工作,其中 x、y 和 z 评估为 a、b 和 c 的解析值。因此,更准确地说,该语言不允许您从用户代码中轻松(或理智地)做到这一点(因为您可以直接从 then 子句返回一个 Promise,您可以将您的值包装在 Promise 中,然后用 Promise 包装这些值.all() 来获得所需的行为,尽管方式很复杂)。 @AustinHemmelgarn 那是错误的,Promise.all 满足 with an array。在Promise.all([a,b]).then((a, b) => bundefined。这就是为什么你需要做.then(([a, b]) => 这是一个解构赋值【参考方案2】:

你可以返回一个包含这两个值的对象——这没有错。

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

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

【讨论】:

可以在另一个then 中返回一个then 吗?它不是一个反模式 ? 就像@robe007 说的,这不是类似于“回调地狱”吗?在这里你的嵌套然后阻塞而不是回调函数,这将违背承诺的目的【参考方案3】:

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

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

另一方面,您可以使用 ES2015 的 destructuring 表达式来获取各个值。

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

【讨论】:

这叫destructuring,不是“deconstructor”,也不是算子:-/ 顺便说一句,您可以在参数中直接使用解构:function step2([server, data]) … - 这样您也可以避免分配给隐式全局变量。而且你真的应该在你的例子中使用returnPromise.resolve,而不是new Promise 构造函数。 感谢@Bergi 的推荐!【参考方案4】:

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

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

【讨论】:

【参考方案5】:

我认为你应该这样做。

拆分链条

因为这两个函数都将使用 amazingData,所以将它们放在一个专用函数中是有意义的。 每次我想重用一些数据时,我通常都会这样做,所以它总是以函数 arg 的形式出现。

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

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

你还会注意到我添加了一个 return 语句,因为它通常是使用 Promises 的方式 - 你总是返回一个 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() 是否正在执行异步操作并不重要。如果是这样,则将返回一个承诺,并且链可以继续。如果不是,则返回结果值。但是因为该函数是从 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 中。


您的编码风格与我过去的风格非常接近,这就是为什么我在 2 年后仍然回答的原因。 我不是到处都有匿名函数的忠实粉丝。我觉得很难读。即使它在社区中很常见。 就像我们用 promise-purgatory 替换了 callback-hell

我还喜欢在 then 中保持函数的名称简短。无论如何,它们只会在本地定义。 大多数时候,他们会调用在别处定义的另一个函数——如此可重用——来完成这项工作。 我什至对只有 1 个参数的函数都这样做,所以当我在函数签名中添加/删除参数时,我不需要进出函数。

吃法

这是一个例子:

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()。这只是创建已解决承诺的快速方法。 我试图通过这个实现的是将我正在运行的所有代码放在一个位置 - 就在 thens 的下方。 所有其他具有更具描述性名称的函数都是可重用的。

这种技术的缺点是它定义了很多函数。 但恐怕这是一种必要的痛苦,以避免到处都有匿名函数。 无论如何风险是什么:堆栈溢出? (笑话!)


使用其他答案中定义的数组或对象也可以。这在某种程度上是answer proposed by Kevin Reid。

您也可以使用 bind()Promise.all()。 请注意,它们仍会要求您拆分代码。

使用绑定

如果你想保持你的函数可重用,但并不真的需要保持 then 里面的内容很短,你可以使用 bind()

function tata( amazingData ) 
    return afterSomething( amazingData )
        .then( afterSomethingElse.bind(null, amazingData) );


function afterSomething( amazingData ) 
    return processAsync( amazingData );

function afterSomethingElse( amazingData, processedData ) 

为简单起见,bind() 将在调用函数时将 args 列表(第一个除外)添加到函数中。

使用 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() - 注意数组的存在 - 只要承诺,但确保没有任何承诺失败,否则它将停止处理。

而不是从 args 参数定义新变量,您应该能够使用 spread() 而不是 then()各种很棒的工作。

【讨论】:

【参考方案6】:

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

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

【讨论】:

【参考方案7】:

无论你从一个承诺返回什么都将被包装成一个承诺,在下一个.then() 阶段被解包。

当您需要返回一个或多个承诺以及一个或多个同步值时会变得很有趣,例如:

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

在这些情况下,必须使用 Promise.all() 在下一个 .then() 阶段解开 p1p2 承诺,例如

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

【讨论】:

【参考方案8】:

你可以勾选Rxjs代表的Observable,让你返回多个值。

【讨论】:

【参考方案9】:

简单地返回一个元组:

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

【讨论】:

以上是关于你如何正确地从 Promise 返回多个值?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确地从函数返回中移动对象?

如何正确处理 Promise 链中的错误?

如何正确地从 innerXMl 获取值

如何正确地从 C++ 中的迭代器返回对对象的引用

如何正确地从 RGB 值初始化 UIColor?

如何在angularjs中链接promise错误函数