如何在 .then() 链中访问之前的承诺结果?

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

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

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

嵌套(和)闭包

使用闭包来维护变量的范围(在我们的例子中,成功回调函数参数)是自然的 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:模块化,使用额外的命名函数,并在不再需要变量时立即展平 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 application, like _.partial from Underscore/lodash or the native .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
}

显式传递

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

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 可以使用解构之前,许多 promise 库(Q, Bluebird, when,...)提供了一个名为 .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
    });
}

而且您可能不仅会使用数组,还会使用任意复杂的对象。例如,在不同的辅助函数中使用 _.extend or Object.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
    });
}

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

ECMAScript 和谐

当然,这个问题也被语言设计者所认识。他们做了很多工作,async functions proposal 终于变成了

ECMAScript 8

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

有专门的库(比如 co or task.js), but also many promise libraries have helper functions (Q, Bluebird, when, …) that do 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

但是,如果您want/need要向后兼容,则不能在没有转译器的情况下使用它们。当前工具支持生成器函数和异步函数,例如参见 [​​=19=].

上的 Babel 文档

然后,还有很多其他的compile-to-JS languages 致力于简化异步编程。他们通常使用类似于 await 的语法(例如 Iced CoffeeScript), but there are also others that feature a Haskell-like do-notation (e.g. LatteJs, monadic, PureScript or LispyScript)。

可变上下文状态

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

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 ugly, and global variables are evil.
  • 这种模式不能跨函数边界工作,模块化函数更难,因为它们的声明不能离开共享范围
  • 变量的范围不会阻止在初始化之前访问它们。对于可能发生竞争条件的复杂承诺结构(循环、分支、异常),这尤其可能发生。显式传递状态,declarative design 承诺鼓励,强制采用更清晰的编码风格来防止这种情况。
  • 必须正确选择这些共享变量的作用域。它需要在执行的函数本地,以防止多个并行调用之间的竞争条件,例如,如果状态存储在实例上,就会出现这种情况。

Bluebird 库鼓励使用传递的对象,使用 their bind() method to assign a context object to a promise chain. It will be accessible from each callback function via the otherwise unusable 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));
}

打破链条

当您需要访问链中的中间值时,您应该将链拆分为您需要的那些单独的部分。与其附加一个回调并以某种方式尝试多次使用其参数,不如将多个回调附加到同一个承诺 - 无论您需要结果值是什么。别忘了,一个promise just represents (proxies) a future value!在线性链中从另一个推导出一个承诺之后,使用您的图书馆提供给您的承诺组合器来构建结果值。

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

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 调用将被许多 promise 库提供的漂亮的辅助方法所取代(Q, Bluebird, when, …): .spread(function(resultA, resultB) { ….

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

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

同步检查

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

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

用户是 promisified Mongoose 模型。

var globalVar = '';

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

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

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

请检查此 link 以获取更多信息:

http://bluebirdjs.com/docs/api/promise.bind.html

Node 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

越简单越好!

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

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

使用局部范围的对象来收集承诺链中的中间结果是解决您提出的问题的合理方法。考虑以下片段:

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

示例在 jsfiddle

上可用

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

这几天也遇到了一些跟你一样的问题。最后,我找到了一个很好的问题解决方案,它简单易读。希望对您有所帮助。

根据how-to-chain-javascript-promises

ok,我们看代码:

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

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

解决方案:

您可以通过使用 '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);

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

我对承诺的了解是仅将其用作 return 值 尽可能避免引用它们。 async/await 语法对此特别实用。今天所有最新的浏览器和节点都支持它: https://caniuse.com/#feat=async-functions ,是一个简单的行为,代码就像阅读同步代码一样,忘记回调......

在某些情况下,我确实需要引用一个承诺,即创建和解决发生在 independent/not-related 个地方。因此,为了解决 "distant" promise,我更愿意将 promise 公开为 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=