如何在.then()链中访问先前的promise结果?

时间:2015-01-31 10:41:30

标签: javascript scope promise bluebird es6-promise

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

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

17 个答案:

答案 0 :(得分:339)

打破链

当您需要访问链中的中间值时,您应该将链条拆分成您需要的那些单件。而不是附加一个回调并以某种方式尝试多次使用其参数,将多个回调附加到同一个承诺 - 无论您需要结果值。别忘了,promise just represents (proxies) a future value!接下来,在线性链中从另一个派生一个promise,使用库提供给你的promise组合器来构建结果值。

这将导致非常简单的控制流程,清晰的功能组合,因此易于模块化。

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

在ES5中只有Promise.all之后的回调中的参数解构,而不是ES5中的then调用将由许多promise库提供的漂亮帮助器方法替换{{1>}。 {3}},QBluebird,...):.spread(function(resultA, resultB) { …

Bluebird还有一个专用的when,可以用更简单(更有效)的结构替换Promise.all + spread组合:

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

答案 1 :(得分:210)

ECMAScript Harmony

当然,这个问题也得到了语言设计者的认可。他们做了很多工作,而async functions proposal终于进入了

ECMAScript 8

您不再需要单个then调用或回调函数,就像在异步函数中(在被调用时返回一个promise),您只需等待promises直接解析即可。它还具有任意控制结构,如条件,循环和try-catch-clauses,但为了方便起见,我们在这里不需要它们:

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带有generator functions,允许在任意放置的yield关键字中将执行分开。这些切片可以相互独立地运行,甚至是异步运行 - 而这正是我们在运行下一步之前等待承诺解析时所做的事情。

有专用的库(如cotask.js),但许多promise库都有辅助函数(QBluebirdwhen,...当你给他们一个产生承诺的生成函数时,为你做this async step-by-step execution

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

但是,如果您希望/需要向后兼容,则无法使用没有转换器的那些。当前工具支持生成器函数和异步函数,例如参见generatorsasync functions上的Babel文档。

然后,还有很多其他compile-to-JS languages 专门用于简化异步编程。它们通常使用类似于await的语法(例如Iced CoffeeScript),但也有一些类似Haskell的do符号(例如LatteJs,{{ 3}},monadicPureScript)。

答案 2 :(得分:97)

同步检查

将promises-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();
    });
}

答案 3 :(得分:51)

嵌套(和)闭包

使用闭包来维护变量的范围(在我们的例子中,成功的回调函数参数)是自然的JavaScript解决方案。有了promises,我们可以任意nest and flatten .then()个回调 - 它们在语义上是等价的,除了内部的范围。

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

当然,这是建立一个缩进金字塔。如果缩进过大,您仍然可以应用旧工具来对抗pyramid of doom:modularize,使用额外的命名函数,并在您不再需要变量时立即压缩承诺链。
从理论上讲,你总是可以避免两个以上的嵌套层次(通过使所有闭包显式化),在实践中使用尽可能多的合理。

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

您还可以使用这种partial application的辅助函数,例如来自Underscore / lodash_.partialnative .bind() method,以进一步减少缩进:

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
}

答案 4 :(得分:48)

显式传递

与嵌套回调类似,此技术依赖于闭包。然而,链条保持平稳 - 而不是仅传递最新结果,每一步都会传递一些状态对象。这些状态对象会累积先前操作的结果,再次传递稍后将需要的所有值以及当前任务的结果。

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]是关闭resultA的函数,并将两个结果的数组传递给下一步。它使用参数解构语法再次在单个变量中分解它。

在ES6提供解构之前,许多承诺库(QBluebirdwhen,...)提供了一个名为.spread()的漂亮助手方法。它需要一个具有多个参数的函数 - 每个数组元素一个 - 用作.spread(function(resultA, resultB) { …

当然,这里所需的闭包可以通过一些辅助函数进一步简化,例如

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

虽然这种模式保证了扁平链条,而显式状态对象可以提高清晰度,但对于长链来说,这将变得乏味。特别是当您偶尔需要状态时,您仍然必须通过每一步。通过这个固定的接口,链中的单个回调相互紧密耦合,并且不灵活。它使得单个步骤的分解变得更加困难,并且不能直接从其他模块提供回调 - 它们总是需要包含在关注状态的样板代码中。像上面这样的抽象辅助函数可以缓解疼痛,但它总会存在。

答案 5 :(得分:32)

可变上下文状态

琐碎(但不优雅且相当错误)的解决方案是只使用更高范围的变量(链中的所有回调都可以访问)并在获得它们时将结果值写入它们:

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

除了许多变量之外,还可以使用(最初为空)对象,在该对象上将结果存储为动态创建的属性。

此解决方案有几个缺点:

  • Mutable state is uglyglobal variables are evil
  • 这种模式在功能边界上不起作用,模块化功能更难,因为它们的声明不能离开共享范围
  • 变量的范围不会阻止在初始化之前访问它们。对于可能发生竞争条件的复杂承诺构造(循环,分支,排除),这尤其可能。明确地传递状态,承诺鼓励的declarative design强制更清晰的编码风格,这可以防止这种情况发生。
  • 必须正确选择这些共享变量的范围。它必须是执行函数的本地函数,以防止多个并行调用之间的竞争条件,例如,如果状态存储在实例上就是这种情况。

Bluebird库鼓励使用传递的对象,使用their bind() method将上下文对象分配给promise链。它可以通过其他不可用的this keyword从每个回调函数访问。虽然对象属性比变量更容易被检测到错别字,但模式非常聪明:

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的promise库中进行模拟(虽然它的表达方式有点冗长,不能在表达式中使用):

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

答案 6 :(得分:9)

对“可变上下文状态”

的不那么强烈的旋转

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

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);
    });
}
  • 全局变量很糟糕,因此该解决方案使用本地范围的变量,不会造成任何伤害。它只能在函数内访问。
  • 可变状态是丑陋的,但这不会以丑陋的方式改变状态。丑陋的可变状态传统上是指修改函数参数或全局变量的状态,但是这种方法只是修改了本地范围的变量的状态,该变量的唯一目的是聚合承诺结果...一个将死于简单死亡的变量一旦承诺解决了。
  • 不会阻止中级承诺访问结果对象的状态,但这并没有引入一些可怕的情况,其中链中的一个承诺将变得流氓并破坏您的结果。在承诺的每个步骤中设置值的责任仅限于此函数,并且整体结果将是正确的或不正确的...它不会是多年后在生产中出现的错误(除非您打算将其用于!)
  • 这不会引入由并行调用引起的竞争条件场景,因为为getExample函数的每次调用都会创建一个新的结果变量实例。

答案 7 :(得分:7)

节点7.4现在支持带有和声标志的async / await调用。

试试这个:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

并使用以下命令运行文件:

node --harmony-async-await getExample.js

简单可以!

答案 8 :(得分:5)

另一个答案,使用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并瞧!

答案 9 :(得分:5)

这一天,我也遇到了一些像你这样的问题。最后,我找到了问题的一个很好的解决方案,它简单易读。我希望这可以帮到你。

根据how-to-chain-javascript-promises

好的,让我们看一下代码:

const firstPromise = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(secondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });

答案 10 :(得分:2)

另一个答案,使用顺序执行器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');
})
&#13;
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
&#13;
&#13;
&#13;

答案 11 :(得分:1)

使用bluebird时,您可以使用.bind方法在promise链中共享变量:

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

答案 12 :(得分:1)

我认为你可以使用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);
    });

答案 13 :(得分:1)

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

答案 14 :(得分:1)

我不会在我自己的代码中使用这种模式,因为我不是使用全局变量的忠实粉丝。然而,在紧要关头它会起作用。

用户是一个有道理的Mongoose模型。

var globalVar = '';

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

答案 15 :(得分:0)

解决方案:

您可以使用“ bind”将中间值显式地放置在任何后续“ then”函数的作用域中。这是一个很好的解决方案,不需要更改Promises的工作方式,只需要一行或两行代码即可传播值,就像已经传播了错误一样。

这是一个完整的示例:

// Get info asynchronously from a server
function pGetServerInfo()
    {
    // then value: "server info"
    } // pGetServerInfo

// Write into a file asynchronously
function pWriteFile(path,string)
    {
    // no then value
    } // pWriteFile

// The heart of the solution: Write formatted info into a log file asynchronously,
// using the pGetServerInfo and pWriteFile operations
function pLogInfo(localInfo)
    {
    var scope={localInfo:localInfo}; // Create an explicit scope object
    var thenFunc=p2.bind(scope); // Create a temporary function with this scope
    return (pGetServerInfo().then(thenFunc)); // Do the next 'then' in the chain
    } // pLogInfo

// Scope of this 'then' function is {localInfo:localInfo}
function p2(serverInfo)
    {
    // Do the final 'then' in the chain: Writes "local info, server info"
    return pWriteFile('log',this.localInfo+','+serverInfo);
    } // p2

可以按以下方式调用此解决方案:

pLogInfo("local info").then().catch(err);

(注意:已经测试了此解决方案的更复杂和完整的版本,但未测试此示例版本,因此可能存在错误。)

答案 16 :(得分:-1)

我了解到的Promise是仅将其用作返回值避免引用它们(如果可能)。异步/等待语法对此特别实用。如今,所有最新的浏览器和节点都支持它:https://caniuse.com/#feat=async-functions,这是一种简单的行为,其代码就像读取同步代码一样,而忘记了回调...

在我确实需要引用的情况下,诺言是在独立/无关的地方进行创建和解决的情况。因此,为了代替“遥远的”承诺,我改为将其作为Deferred公开,而不是人为的关联,可能还有事件侦听器,以下代码在有效的es5中实现了它

/**
 * Promise like object that allows to resolve it promise from outside code. Example:
 *
```
class Api {
  fooReady = new Deferred<Data>()
  private knower() {
    inOtherMoment(data=>{
      this.fooReady.resolve(data)
    })
  }
}
```
 */
var Deferred = /** @class */ (function () {
  function Deferred(callback) {
    var instance = this;
    this.resolve = null;
    this.reject = null;
    this.status = 'pending';
    this.promise = new Promise(function (resolve, reject) {
      instance.resolve = function () { this.status = 'resolved'; resolve.apply(this, arguments); };
      instance.reject = function () { this.status = 'rejected'; reject.apply(this, arguments); };
    });
    if (typeof callback === 'function') {
      callback.call(this, this.resolve, this.reject);
    }
  }
  Deferred.prototype.then = function (resolve) {
    return this.promise.then(resolve);
  };
  Deferred.prototype.catch = function (r) {
    return this.promise.catch(r);
  };
  return Deferred;
}());

从我的打字稿项目中编译下来:

https://github.com/cancerberoSgx/misc-utils-of-mine/blob/2927c2477839f7b36247d054e7e50abe8a41358b/misc-utils-of-mine-generic/src/promise.ts#L31

对于更复杂的情况,我经常使用这些家伙小承诺工具,而无需测试和键入依赖项。 p-map已经有用了好几次了。我认为他涵盖了大多数用例:

https://github.com/sindresorhus?utf8=%E2%9C%93&tab=repositories&q=promise&type=source&language=