725

我已经将我的代码重组为Promise,并构建了一个美妙的长而扁平的 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?
    });
}
4

17 回答 17

417
于 2015-01-31T10:44:16.490 回答
263

ECMAScript Harmony

Of course, this problem was recognized by the language designers as well. They did a lot of work and the async functions proposal finally made it into

ECMAScript 8

You don't need a single then invocation or callback function anymore, as in an asynchronous function (that returns a promise when being called) you can simply wait for promises to resolve directly. It also features arbitrary control structures like conditions, loops and try-catch-clauses, but for the sake of convenience we don't need them here:

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

While we were waiting for ES8, we already did use a very similar kind of syntax. ES6 came with generator functions, which allow breaking the execution apart in pieces at arbitrarily placed yield keywords. Those slices can be run after each other, independently, even asynchronously - and that's just what we do when we want to wait for a promise resolution before running the next step.

There are dedicated libraries (like co or task.js), but also many promise libraries have helper functions (Q, Bluebird, when, …) that do this async step-by-step execution for you when you give them a generator function that yields promises.

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

This did work in Node.js since version 4.0, also a few browsers (or their dev editions) did support generator syntax relatively early.

ECMAScript 5

However, if you want/need to be backward-compatible you cannot use those without a transpiler. Both generator functions and async functions are supported by the current tooling, see for example the documentation of Babel on generators and async functions.

And then, there are also many other compile-to-JS languages that are dedicated to easing asynchronous programming. They usually use a syntax similar to await, (e.g. Iced CoffeeScript), but there are also others that feature a Haskell-like do-notation (e.g. LatteJs, monadic, PureScript or LispyScript).

于 2015-01-31T10:43:21.823 回答
103

同步检查

将 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();
    });
}
于 2015-01-31T13:16:31.257 回答
62

嵌套(和)闭包

使用闭包来维护变量的范围(在我们的例子中,成功回调函数参数)是自然的 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;
        });
    });
}

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

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

您还可以对这种部分应用程序使用辅助函数,例如_.partialUnderscore / 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
}
于 2015-01-31T10:42:23.393 回答
55
于 2015-01-31T10:42:52.400 回答
37

Mutable contextual state

The trivial (but inelegant and rather errorprone) solution is to just use higher-scope variables (to which all callbacks in the chain have access) and write result values to them when you get them:

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

Instead of many variables one might also use an (initially empty) object, on which the results are stored as dynamically created properties.

This solution has several drawbacks:

  • Mutable state is ugly, and global variables are evil.
  • This pattern doesn't work across function boundaries, modularising the functions is harder as their declarations must not leave the shared scope
  • The scope of the variables does not prevent to access them before they are initialized. This is especially likely for complex promise constructions (loops, branching, excptions) where race conditions might happen. Passing state explicitly, a declarative design that promises encourage, forces a cleaner coding style which can prevent this.
  • One must choose the scope for those shared variables correctly. It needs to be local to the executed function to prevent race conditions between multiple parallel invocations, as would be the case if, for example, state was stored on an instance.

The Bluebird library encourages the use of an object that is passed along, using 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. While object properties are more prone to undetected typos than variables, the pattern is quite clever:

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
}

This approach can be easily simulated in promise libraries that do not support .bind (although in a somewhat more verbose way and cannot be used in an expression):

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));
}
于 2015-01-31T10:43:50.990 回答
20

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

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

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 访问结果对象的状态,但这不会引入一些可怕的场景,即链中的一个 promise 会变得流氓并破坏您的结果。在 Promise 的每个步骤中设置值的责任仅限于此函数,并且总体结果将是正确的或不正确的......它不会是几年后在生产中出现的错误(除非你打算这样做!)
  • 这不会引入由并行调用引起的竞争条件场景,因为每次调用 getExample 函数都会创建一个结果变量的新实例。

示例可在jsfiddle上找到

于 2017-03-24T20:08:13.853 回答
8

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

尽可能简单!

于 2017-01-21T22:14:57.927 回答
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()

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

于 2015-11-20T19:59:46.967 回答
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);
    });
于 2017-07-25T06:34:25.957 回答
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');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

于 2017-06-10T00:56:57.913 回答
2

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

User 是一个承诺的 Mongoose 模型。

var globalVar = '';

User.findAsync({}).then(function(users){
  globalVar = users;
}).then(function(){
  console.log(globalVar);
});
于 2015-08-11T18:35:55.933 回答
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

于 2016-06-12T06:33:03.867 回答
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);
    });
于 2017-08-29T10:34:10.440 回答
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

于 2017-03-03T09:45:55.797 回答
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);

(注意:这个解决方案的一个更复杂和完整的版本已经过测试,但不是这个示例版本,所以它可能有一个错误。)

于 2019-08-27T20:17:03.850 回答
-1

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

如果我确实需要引用一个承诺,那就是创建和解决发生在独立/不相关的地方。因此,取而代之的是一个人工关联,可能是一个事件侦听器,只是为了解决“遥远的”承诺,我更喜欢将承诺公开为延迟,以下代码在有效的 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=

于 2019-09-12T20:37:05.893 回答