我将代码重构为承诺,并构建了一个奇妙的长而平坦的承诺链,由多个.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?
    });
}

嵌套(和)闭包

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

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

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

对于这类局部应用程序,您还可以使用helper函数,如_。来自Underscore/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
}

显式直通

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

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提供解构之前,许多承诺库(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。所有这些都是为了产生数组的承诺:

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

你不仅可以使用数组,还可以使用任意复杂的对象。例如,用_。扩展或对象。在不同的helper函数中赋值:

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

While this pattern guarantees a flat chain and explicit state objects can improve clarity, it will become tedious for a long chain. Especially when you need the state only sporadically, you still have to pass it through every step. With this fixed interface, the single callbacks in the chain are rather tightly coupled and inflexible to change. It makes factoring out single steps harder, and callbacks cannot be supplied directly from other modules - they always need to be wrapped in boilerplate code that cares about the state. Abstract helper functions like the above can ease the pain a bit, but it will always be present.


ECMAScript和谐

当然,语言设计者也认识到了这个问题。他们做了大量的工作,异步函数提案最终得以通过

ECMAScript 8

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

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关键字处将执行分开。这些切片可以彼此独立地、甚至异步地运行——当我们想要在运行下一步之前等待承诺解决时,我们就是这样做的。

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

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

这在Node.js 4.0版本起就可以了,也有一些浏览器(或他们的开发版本)相对较早地支持生成器语法。

ECMAScript 5

然而,如果你想要/需要向后兼容,你就不能在没有转译器的情况下使用它们。当前工具支持生成器函数和异步函数,例如,请参阅关于生成器和异步函数的Babel文档。

此外,还有许多其他的编译到js语言 它们致力于简化异步编程。它们通常使用类似于await的语法(例如Iced CoffeeScript),但也有其他一些具有类似haskell的do-notation(例如LatteJs, monadic, PureScript或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. 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.

Bluebird库鼓励使用传递的对象,使用bind()方法将上下文对象分配给承诺链。每个回调函数都可以通过不可用的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
}

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

打破枷锁

When you need to access the intermediate values in your chain, you should split your chain apart in those single pieces that you need. Instead of attaching one callback and somehow trying to use its parameter multiple times, attach multiple callbacks to the same promise - wherever you need the result value. Don't forget, a promise just represents (proxies) a future value! Next to deriving one promise from the other in a linear chain, use the promise combinators that are given to you by your library to build the result 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之后的回调中进行参数解构。在ES5中,then调用将被许多承诺库(Q, Bluebird, when,…)提供的一个漂亮的助手方法所取代:.spread(function(resultA, resultB) {....

Bluebird还提供了一个专用的连接功能来取代承诺。All +扩展组合,更简单(更有效)的结构:

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

同步检查

将稍后需要的承诺值分配给变量,然后通过同步检查获得它们的值。这个例子使用了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();
    });
}

我不打算在我自己的代码中使用这种模式,因为我不太喜欢使用全局变量。然而,在紧要关头它还是有用的。

用户是一个承诺的猫鼬模型。

var globalVar = '';

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

另一个答案,使用babel节点版本<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方法来共享承诺链中的变量:

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


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

Global variables are bad, so this solution uses a locally scoped variable which causes no harm. It is only accessible within the function. Mutable state is ugly, but this does not mutate state in an ugly manner. The ugly mutable state traditionally refers to modifying the state of function arguments or global variables, but this approach simply modifies the state of a locally scoped variable that exists for the sole purpose of aggregating promise results...a variable that will die a simple death once the promise resolves. Intermediate promises are not prevented from accessing the state of the results object, but this does not introduce some scary scenario where one of the promises in the chain will go rogue and sabotage your results. The responsibility of setting the values in each step of the promise is confined to this function and the overall result will either be correct or incorrect...it will not be some bug that will crop up years later in production (unless you intend it to!) This does not introduce a race condition scenario that would arise from parallel invocation because a new instance of the results variable is created for every invocation of the getExample function.

示例在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

好的,让我们看看代码:

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'函数的作用域中。这是一个很好的解决方案,它不需要改变promise的工作方式,只需要一两行代码来传播这些值,就像已经传播错误一样。

下面是一个完整的例子:

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

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


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

在我确实需要引用承诺的情况下,创建和解决发生在独立/不相关的地方。因此,我宁愿使用人工关联和事件侦听器来解析“远程”承诺,而更喜欢将承诺公开为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;
}());

编译自我的一个typescript项目:

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=