关于javascript:如何在.then()链中访问以前的promise结果?

How do I access previous promise results in a .then() chain?

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

1
2
3
4
5
6
7
8
9
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?
    });
}


打破链条

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

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

1
2
3
4
5
6
7
8
9
10
11
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.all之后回调中的参数解构只是变得有用和害羞;能够与ES6一起使用,在ES5中,then调用将被许多承诺库提供的漂亮助手方法所取代(Q,Bluebird,何时, ......):.spread(function(resultA, resultB) { …

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

1
2

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


ECMAScript Harmony

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

ECMAScript 8

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

1
2
3
4
5
6
7
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解析时所做的事情。

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

1
2
3
4
5
6
7
8
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 on generator和async函数的文档。

然后,还有许多其他编译到JS的语言
专门用于简化异步编程。它们通常使用类似于await的语法(例如Iced CoffeeScript),但也有一些语法具有类似Haskell的do注释(例如LatteJs,monadic,PureScript或LispyScript)。


同步检查

将promises-for-later-needed-values分配给变量,然后通过同步检查获取它们的值。该示例使用bluebird的.value()方法,但许多库提供了类似的方法。

1
2
3
4
5
6
7
8
9
10
11
12
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();
    });
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
    });
}


嵌套(和)闭包

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

1
2
3
4
5
6
7
8
9
function getExample() {
    return promiseA().then(function(resultA) {
        // some processing
        return promiseB().then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
    };
}

您还可以为这种部分应用程序使用辅助函数,例如来自Underscore / lodash的_.partial或本机.bind()方法,以进一步减少缩进:

1
2
3
4
5
6
7
8
9
10
11
12
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
}


明确的传递

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

1
2
3
4
5
6
7
8
9
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()的漂亮助手方法是亲和害羞的; vi­ ded由许多承诺库(Q,Bluebird,当,......)。它需要一个具有多个参数的函数 - 每个数组元素一个 - 用作.spread(function(resultA, resultB) { …

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

1
2
3
4
5
6
7
8
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来生成数组的承诺:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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
    });
}

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


可变的上下文状态

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

1
2
3
4
5
6
7
8
9
10
11
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
    });
}

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

该解决方案有几个缺点:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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库中进行模拟(尽管以更冗长的方式并且不能在表达式中使用):

1
2
3
4
5
6
7
8
9
10
11
12
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));
}


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

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

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


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

根据how-to-chain-javascript-promises

好的,让我们来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
    });


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

试试这个:

1
2
3
4
5
6
7
8
9
10
11
async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

并运行该文件:

node --harmony-async-await getExample.js

简单就可以了!


另一个答案,使用babel-node版本<6

使用async - await

npm install -g [email protected]

example.js:

1
2
3
4
5
6
7
8
9
10
11
async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

然后,运行babel-node example.js并瞧!


另一个答案,使用顺序执行器nsynjs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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');
})

更新:添加了工作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');
})
1
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js">


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

用户是一个有说服力的Mongoose模型。

1
2
3
4
5
6
7
var globalVar = '';

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


我想你可以使用RSVP的哈希。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    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);
    });


1
2
3
4
5
6
7
8
9
10
11
12
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


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

1
2
3
4
5
6
7
8
9
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