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

IT技术 javascript scope promise bluebird es6-promise
2020-12-21 00:08:08

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

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?
    });
}
6个回答

打破链条

当您需要访问链中的中间值时,您应该将链拆分为您需要的单个部分。与其附加一个回调并以某种方式尝试多次使用其参数,不如将多个回调附加到同一个 Promise - 无论您需要结果值的任何地方。不要忘记,promise 只是代表(代理)一个未来值在从线性链中的另一个 promise 派生一个 promise 之后,使用您的库提供给您的 promise 组合子来构建结果值。

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

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.allES6之后回调中的参数解构,在 ES5 中,then调用将被许多Promise库(QBluebirdwhen,...)提供的漂亮帮助方法替换.spread(function(resultA, resultB) { …

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

…
return Promise.join(a, b, function(resultA, resultB) { … });
@scaryguy:数组中没有函数,那些是Promise。promiseA并且promiseB是(返回Promise的)函数。
2021-02-07 00:08:08
数组中的函数是否按顺序执行?
2021-02-11 00:08:08
@reify 不,你不应该那样做,这会给拒绝带来麻烦。
2021-02-12 00:08:08
@Roland 从来没有说过它是:-) 这个答案是在 ES5 时代写的,当时标准中根本没有Promise,并且spread在这种模式中非常有用。有关更现代的解决方案,请参阅已接受的答案。然而,我已经更新了显式直通答案,而且真的没有充分的理由不更新这个答案
2021-02-23 00:08:08
我不明白这个例子。如果有一串“then”语句要求在整个链中传播值,我看不出这如何解决问题。在该值存在之前,不能触发(创建)需要先前值的 Promise。此外, Promise.all() 只是等待其列表中的所有Promise完成:它不会强加任何顺序。所以我需要每个“下一个”函数来访问所有以前的值,我看不出你的例子是如何做到的。你应该通过你的例子引导我们,因为我不相信或理解它。
2021-02-28 00:08:08

ECMAScript 和谐

当然,这个问题也得到了语言设计者的认可。他们做了很多工作,异步函数提案最终成为了

ECMAScript 8

您不再需要单个then调用或回调函数,因为在异步函数(在被调用时返回Promise)中,您可以简单地等待Promise直接解析。它还具有任意控制结构,如条件、循环和 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关键字处将执行分开这些切片可以相互独立,甚至异步运行 - 这就是我们想要在运行下一步之前等待Promise解决方案时所做的。

有专用的库(如cotask.js),但也有许多Promise库具有辅助函数(QBluebirdwhen ……),当您给它们一个生成器函数时,它们会为您执行异步逐步执行产生Promise。

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

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

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

@Bergi 您是否需要等待来自外部代码的异步函数示例 getExample()?
2021-02-08 00:08:08
我很好奇,为什么你问完后立即回答自己的问题?这里有一些很好的讨论,但我很好奇。也许您在询问后自己找到了答案?
2021-02-11 00:08:08
在带有生成器函数的 ECMAScript 6 示例中,是否有一种(不太费力的)方法可以避免使用 Promise.coroutine(即,不使用 Bluebird 或其他库,而仅使用纯 JS)?我想到了类似的东西,steps.next().value.then(steps.next)...但没有用。
2021-02-12 00:08:08
@granmoe:我故意将整个讨论发布为规范的重复目标
2021-02-17 00:08:08
@arisalexis:是的,getExample它仍然是一个返回Promise的函数,就像其他答案中的函数一样工作,但语法更好。您可以await调用另一个async函数,也可以链接.then()到它的结果。
2021-03-08 00:08:08

同步检测

将 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();
    });
}
这是我最喜欢的答案:可读、可扩展且对库或语言功能的依赖最小
2021-02-11 00:08:08
@Jason:呃,“对库功能的依赖最小”?同步检查是一个库功能,并且是一个非常非标准的启动功能。
2021-02-24 00:08:08
我认为他的意思是图书馆特定的功能
2021-03-08 00:08:08

嵌套(和)闭包

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

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

当然,这是在构建一个缩进金字塔。如果缩进变得太大,您仍然可以应用旧工具来对抗厄运金字塔:module化,使用额外的命名函数,并在不再需要变量时立即展平Promise链。
理论上,您总是可以避免多于两层的嵌套(通过使所有闭包显式),在实践中尽可能多地使用。

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下划线/ 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
}
在 Nolan Lawson 关于 promises pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html的文章中,同样的建议作为“高级错误 #4”的解决方案给出这是一个很好的阅读。
2021-02-08 00:08:08
这正是bindMonads 中功能。Haskell 提供了语法糖(do-notation)使其看起来像 async/await 语法。
2021-02-20 00:08:08

显式传递

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

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 提供解构之前.spread(),许多 Promise 库(QBluebirdwhen ……)都提供了一个漂亮的辅助方法调用它需要一个带有多个参数的函数 - 每个数组元素一个 - 用作.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生成数组的Promise:

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

虽然这种模式保证了一个扁平的链并且明确的状态对象可以提高清晰度,但对于长链来说它会变得乏味。特别是当你只是偶尔需要状态时,你仍然需要每一步都通过它。有了这个固定的接口,链中的单个回调就非常紧密地耦合在一起,并且无法灵活改变。它使分解单个步骤变得更加困难,并且不能直接从其他module提供回调——它们总是需要包装在关心状态的样板代码中。像上面这样的抽象辅助函数可以稍微缓解一些痛苦,但它会一直存在。

通过数组语法,我的意思return [x,y]; }).spread(...return Promise.all([x, y]); }).spread(...,当为 es6 解构糖交换传播时它不会改变,并且也不会是一个奇怪的边缘情况,Promise将返回的数组与其他任何东西都不同。
2021-02-09 00:08:08
这可能是最好的答案。Promises 是“函数式反应式编程”-轻量级,这通常是采用的解决方案。例如,BaconJs 有 #combineTemplate,它允许您将结果组合成一个对象,并在链中向下传递
2021-02-17 00:08:08
@BenjaminGruenbaum:“省略语法Promise.all是什么意思此答案中的任何方法都不会破坏 ES6。spread将 a切换到解构then也不应该有问题。Re .prototype.augment:我知道有人会注意到它,我只是喜欢探索可能性 - 将其编辑掉。
2021-03-02 00:08:08
首先,我认为不Promise.all应该鼓励省略的语法(当解构将替换它并将 a 切换.spread到 a时,它不会在 ES6 中工作,then通常会给人们意想不到的结果。至于增加 - 我不知道你为什么需要使用增加 - 向Promise原型添加东西不是扩展 ES6 Promise的一种可接受的方式,这些Promise应该使用(当前不受支持的)子类进行扩展。
2021-03-06 00:08:08
@CapiEtheriel 答案是在 ES6 不像今天这样广泛传播的时候写的。是的,也许是时候交换示例了
2021-03-08 00:08:08