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

Ber*_*rgi 607 javascript scope promise bluebird es6-promise

我已经将我的代码重组为承诺,并构建了一个由多个回调组成的精彩长扁平承诺链.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?
    });
}
Run Code Online (Sandbox Code Playgroud)

Ber*_*rgi 358

打破链条

当您需要访问链中的中间值时,您应该将链条拆分成您需要的那些单件.而不是附加一个回调并以某种方式尝试多次使用其参数,将多个回调附加到同一个承诺 - 无论您需要结果值.不要忘记,承诺只代表(代理)未来的价值!接下来,在线性链中从另一个派生一个承诺,使用库提供给您的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
    });
}
Run Code Online (Sandbox Code Playgroud)

Promise.all在ES5中只有ES6可用的回调中的参数解构代替,在ES5中,then调用将被许多promise库(Q,Bluebird,when,...)提供的漂亮的辅助方法取代:.spread(function(resultA, resultB) { ….

Bluebird还具有专用join功能,可以用更简单(更高效)的构造替换Promise.all+ spread组合:

…
return Promise.join(a, b, function(resultA, resultB) { … });
Run Code Online (Sandbox Code Playgroud)

  • @scaryguy:数组中没有函数,那些是promises.`promiseA`和`promiseB`是这里的(承诺返回)函数. (6认同)
  • @Roland从没说过:-)这个答案是在ES5时代写的,当时根本没有任何标准,在这种模式下`spread'超级有用。有关更现代的解决方案,请参见公认的答案。但是,我已经更新了[显式传递的答案](/sf/answers/1977548541/),确实没有充分的理由不更新此答案。 (2认同)
  • 我不明白这个例子。如果存在一系列“then”语句,要求值在整个链中传播,我不知道这如何解决问题。在该值存在之前,需要先前值的 Promise 无法被触发(创建)。此外,Promise.all() 只是等待其列表中的所有 Promise 完成:它不强加顺序。所以我需要每个“下一个”函数来访问所有以前的值,但我不明白你的示例是如何做到这一点的。你应该向我们展示你的例子,因为我不相信也不理解它。 (2认同)

Ber*_*rgi 221

ECMAScript Harmony

当然,这个问题也得到了语言设计者的认可.他们做了很多工作,异步功能提案终于成功了

ECMAScript 8

您不再需要单个then调用或回调函数,因为在异步函数(在调用时返回一个promise)中,您只需等待promises直接解析即可.它还具有任意控制结构,如条件,循环和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
}
Run Code Online (Sandbox Code Playgroud)

ECMAScript 6

在我们等待ES8的同时,我们已经使用了非常类似的语法.ES6带有生成器功能,允许在任意放置的yield关键字中将执行分开.这些切片可以相互独立地运行,甚至是异步运行 - 而这正是我们在运行下一步之前等待promise解析时所做的事情.

有专门的库(比如cotask.js),但是许多promise库都有辅助函数(Q,Bluebird,when,...),当你给它们一个生成器函数时,它会为你执行这个async逐步执行产生承诺.

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
});
Run Code Online (Sandbox Code Playgroud)

从版本4.0开始,这在Node.js中工作,也有一些浏览器(或他们的开发版)相对较早地支持生成器语法.

ECMAScript 5

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

然后,还有许多其他编译到JS的语言 专门用于简化异步编程.他们通常使用类似语法await(例如冰的CoffeeScript),但也有其他人配备了哈斯克尔样do-notation(如LatteJs,一元,PureScriptLispyScript).


Esa*_*ija 100

同步检查

将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();
    });
}
Run Code Online (Sandbox Code Playgroud)

这可以用于任意数量的值:

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();
    });
}
Run Code Online (Sandbox Code Playgroud)

  • @Jason:呃,"*对图书馆功能的依赖最少*"?同步检查是一种库功能,是一种非常标准的引导功能. (12认同)
  • 这是我最喜欢的答案:可读,可扩展和最少依赖库或语言功能 (6认同)
  • 我认为他是指图书馆的特定功能 (2认同)

Ber*_*rgi 53

嵌套(和)闭包

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

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

当然,这是建立一个缩进金字塔.如果缩进变得太大,你仍然可以使用旧工具来对抗厄运金字塔:模块化,使用额外的命名函数,并在不再需要变量时立即压缩承诺链.
理论上,你总是可以避免两个以上的嵌套级别(通过使所有闭包显式化),在实践中使用尽可能多的合理.

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
    };
}
Run Code Online (Sandbox Code Playgroud)

您还可以使用辅助功能对于这种局部的应用,如_.partial下划线/lodash本地.bind()方法,进一步减少缩进:

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
}
Run Code Online (Sandbox Code Playgroud)

  • 同样的建议是Nolan Lawson关于承诺的文章中的'高级错误#4'的解决方案http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html.这是一个很好的阅读. (5认同)
  • 这正是Monads中的`bind`函数.Haskell提供语法糖(do-notation)使其看起来像async/await语法. (2认同)

Ber*_*rgi 49

明确的传递

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

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
    });
}
Run Code Online (Sandbox Code Playgroud)

这里,小箭头b => [resultA, b]是关闭的函数,resultA并将两个结果的数组传递给下一步.其中使用参数解构语法再次将其分解为单个变量.

在ES6进行解构之前.spread(),许多promise库(Q,Bluebird,when,......)提供了一个很好的辅助方法.它需要一个带有多个参数的函数 - 每个数组元素一个 - 用作.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));
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用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
    });
}
Run Code Online (Sandbox Code Playgroud)

而且您可能不仅使用数组,而且使用任意复杂的对象.例如,使用_.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
    });
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这可能是最好的答案.承诺是"功能反应式编程" - 轻微,这通常是采用的解决方案.例如,BaconJs有#combineTemplate,它允许你将结果组合成一个传递给链的对象 (3认同)

Ber*_*rgi 35

可变的上下文状态

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

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
    });
}
Run Code Online (Sandbox Code Playgroud)

可以使用(最初为空的)对象而不是许多变量,在该对象上将结果存储为动态创建的属性.

该解决方案有几个缺点:

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

这种方法可以很容易地在不支持.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));
}
Run Code Online (Sandbox Code Playgroud)

  • 请将这个答案分成两部分,因为我差点在序言中投票!我认为"琐碎(但不优雅且相当错误)的解决方案"是最简洁和最简单的解决方案,因为它不再依赖于闭包和可变状态而不是您接受的自我回答,但更简单.关闭不是全球性的,也不是邪恶的.鉴于这个前提,对这种方法的论据对我来说毫无意义.什么模块化问题可以给出一个"美妙的长扁平承诺链"? (4认同)
  • 正如我上面所说,Promises是"功能反应式编程"-light.这是FRP中的反模式 (2认同)

Jay*_*Jay 14

对"可变上下文状态"的不那么苛刻的旋转

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

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(paramsA).then(function(resultA){
        results.a = resultA;
        return promiseB(paramsB);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(paramsC);
    }).then(function(resultC){
        //Resolve with composite of all promises
        return Promise.resolve(results.a + results.b + resultC);
    }).catch(function(error){
        return Promise.reject(error);
    });
}
Run Code Online (Sandbox Code Playgroud)
  • 全局变量很糟糕,因此该解决方案使用本地范围的变量,这不会造成任何伤害.它只能在函数内访问.
  • 可变状态是丑陋的,但这不会以丑陋的方式改变状态.丑陋的可变状态传统上是指修改函数参数或全局变量的状态,但是这种方法只是修改了本地范围的变量的状态,该变量的唯一目的是聚合承诺结果...一个将死于简单死亡的变量一旦承诺解决了.
  • 中级承诺不会被阻止访问结果对象的状态,但这并没有引入一些可怕的情况,其中链中的一个承诺将变得流氓并破坏您的结果.在承诺的每个步骤中设置值的责任仅限于此函数,并且整体结果将是正确的或不正确的...它不会是多年后在生产中出现的错误(除非您打算将其用于!)
  • 这不会引入由并行调用引起的竞争条件场景,因为为getExample函数的每次调用都会创建一个新的结果变量实例.


Ant*_*ony 7

节点7.4现在支持带有和声标志的异步/等待调用.

试试这个:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()
Run Code Online (Sandbox Code Playgroud)

并运行该文件:

node --harmony-async-await getExample.js

简单就可以了!


yzf*_*zwl 7

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

根据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);
    });
Run Code Online (Sandbox Code Playgroud)

  • 这并没有真正回答关于如何访问链中的先前结果的问题. (4认同)
  • 每个承诺都可以获得以前的价值,你的意思是什么? (2认同)
  • 看一下问题中的代码。目的不是获得调用 `.then` 的承诺的结果,而是获得之前的结果。例如`thirdPromise` 访问`firstPromise` 的结果。 (2认同)

Ant*_*ony 6

另一个答案,使用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()
Run Code Online (Sandbox Code Playgroud)

然后,跑babel-node example.js,瞧!