如何正确地从一个项目返回多个值?

我最近遇到过几次这样的情况,我不知道如何正确地解决它。假设以下代码:

somethingAsync()
.then( afterSomething )
.then( afterSomethingElse )
  

function afterSomething( amazingData ) {
return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
}

现在可能会出现这样一种情况: 我希望能够访问 afterSomethingElse中的 amazingData

一个显而易见的解决方案是从 afterSomething返回数组或散列,因为您只能从函数返回一个值。但是我想知道是否有一种方法可以让 afterSomethingElse接受2个参数并同样地调用它,因为这似乎更容易记录和理解。

我只是想知道这种可能性,因为有 Q.spread,它做一些类似于我想要的东西。

147570 次浏览

You can't resolve a promise with multiple properties just like you can't return multiple values from a function. A promise conceptually represents a value over time so while you can represent composite values you can't put multiple values in a promise.

A promise inherently resolves with a single value - this is part of how Q works, how the Promises/A+ spec works and how the abstraction works.

The closest you can get is use Q.spread and return arrays or use ES6 destructuring if it's supported or you're willing to use a transpilation tool like BabelJS.

As for passing context down a promise chain please refer to Bergi's excellent canonical on that.

You can return an object containing both values — there's nothing wrong with that.

Another strategy is to keep the value, via closures, instead of passing it through:

somethingAsync().then(afterSomething);


function afterSomething(amazingData) {
return processAsync(amazingData).then(function (processedData) {
// both amazingData and processedData are in scope here
});
}

Fully rather than partially inlined form (equivalent, arguably more consistent):

somethingAsync().then(function (amazingData) {
return processAsync(amazingData).then(function (processedData) {
// both amazingData and processedData are in scope here
});
}

you can only pass one value, but it can be an array with multiples values within, as example:

function step1(){
let server = "myserver.com";
let data = "so much data, very impresive";
return Promise.resolve([server, data]);
}

on the other side, you can use the destructuring expression for ES2015 to get the individual values.

function step2([server, data]){
console.log(server); // print "myserver.com"
console.log(data);   // print "so much data, very impresive"
return Promise.resolve("done");
}

to call both promise, chaining them:

step1()
.then(step2)
.then((msg)=>{
console.log(msg); // print "done"
})

Two things you can do, return an object

somethingAsync()
.then( afterSomething )
.then( afterSomethingElse );


function processAsync (amazingData) {
//processSomething
return {
amazingData: amazingData,
processedData: processedData
};
}


function afterSomething( amazingData ) {
return processAsync( amazingData );
}


function afterSomethingElse( dataObj ) {
let amazingData = dataObj.amazingData,
processedData = dataObj.proccessedData;
}

Use the scope!

var amazingData;
somethingAsync()
.then( afterSomething )
.then( afterSomethingElse )


function afterSomething( returnedAmazingData ) {
amazingData = returnedAmazingData;
return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
//use amazingData here
}

Here is how I reckon you should be doing.

splitting the chain

Because both functions will be using amazingData, it makes sense to have them in a dedicated function. I usually do that everytime I want to reuse some data, so it is always present as a function arg.

As your example is running some code, I will suppose it is all declared inside a function. I will call it toto(). Then we will have another function which will run both afterSomething() and afterSomethingElse().

function toto() {
return somethingAsync()
.then( tata );
}

You will also notice I added a return statement as it is usually the way to go with Promises - you always return a promise so we can keep chaining if required. Here, somethingAsync() will produce amazingData and it will be available everywhere inside the new function.

Now what this new function will look like typically depends on is processAsync() also asynchronous?

processAsync not asynchronous

No reason to overcomplicate things if processAsync() is not asynchronous. Some old good sequential code would make it.

function tata( amazingData ) {
var processed = afterSomething( amazingData );
return afterSomethingElse( amazingData, processed );
}


function afterSomething( amazingData ) {
return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

Note that it does not matter if afterSomethingElse() is doing something async or not. If it does, a promise will be returned and the chain can continue. If it is not, then the result value will be returned. But because the function is called from a then(), the value will be wrapped into a promise anyway (at least in raw Javascript).

processAsync asynchronous

If processAsync() is asynchronous, the code will look slightly different. Here we consider afterSomething() and afterSomethingElse() are not going to be reused anywhere else.

function tata( amazingData ) {
return afterSomething()
.then( afterSomethingElse );


function afterSomething( /* no args */ ) {
return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
/* amazingData can be accessed here */
}
}

Same as before for afterSomethingElse(). It can be asynchronous or not. A promise will be returned, or a value wrapped into a resolved promise.


Your coding style is quite close to what I use to do, that is why I answered even after 2 years. I am not a big fan of having anonymous functions everywhere. I find it hard to read. Even if it is quite common in the community. It is as we replaced the callback-hell by a promise-purgatory.

I also like to keep the name of the functions in the then short. They will only be defined locally anyway. And most of the time they will call another function defined elsewhere - so reusable - to do the job. I even do that for functions with only 1 parameter, so I do not need to get the function in and out when I add/remove a parameter to the function signature.

Eating example

Here is an example:

function goingThroughTheEatingProcess(plenty, of, args, to, match, real, life) {
return iAmAsync()
.then(chew)
.then(swallow);


function chew(result) {
return carefullyChewThis(plenty, of, args, "water", "piece of tooth", result);
}


function swallow(wine) {
return nowIsTimeToSwallow(match, real, life, wine);
}
}


function iAmAsync() {
return Promise.resolve("mooooore");
}


function carefullyChewThis(plenty, of, args, and, some, more) {
return true;
}


function nowIsTimeToSwallow(match, real, life, bobool) {
}

Do not focus too much on the Promise.resolve(). It is just a quick way to create a resolved promise. What I try to achieve by this is to have all the code I am running in a single location - just underneath the thens. All the others functions with a more descriptive name are reusable.

The drawback with this technique is that it is defining a lot of functions. But it is a necessary pain I am afraid in order to avoid having anonymous functions all over the place. And what is the risk anyway: a stack overflow? (joke!)


Using arrays or objects as defined in other answers would work too. This one in a way is the answer proposed by Kevin Reid.

You can also use bind() or Promise.all(). Note that they will still require you to split your code.

using bind

If you want to keep your functions reusable but do not really need to keep what is inside the then very short, you can use bind().

function tata( amazingData ) {
return afterSomething( amazingData )
.then( afterSomethingElse.bind(null, amazingData) );
}


function afterSomething( amazingData ) {
return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

To keep it simple, bind() will prepend the list of args (except the first one) to the function when it is called.

using Promise.all

In your post you mentionned the use of spread(). I never used the framework you are using, but here is how you should be able to use it.

Some believe Promise.all() is the solution to all problems, so it deserves to be mentioned I guess.

function tata( amazingData ) {
return Promise.all( [ amazingData, afterSomething( amazingData ) ] )
.then( afterSomethingElse );
}


function afterSomething( amazingData ) {
return processAsync( amazingData );
}
function afterSomethingElse( args ) {
var amazingData = args[0];
var processedData = args[1];
}

You can pass data to Promise.all() - note the presence of the array - as long as promises, but make sure none of the promises fail otherwise it will stop processing.

And instead of defining new variables from the args argument, you should be able to use spread() instead of then() for all sort of awesome work.

Simply make an object and extract arguments from that object.

let checkIfNumbersAddToTen = function (a, b) {
return new Promise(function (resolve, reject) {
let c = parseInt(a)+parseInt(b);
let promiseResolution = {
c:c,
d : c+c,
x : 'RandomString'
};
if(c===10){
resolve(promiseResolution);
}else {
reject('Not 10');
}
});
};

Pull arguments from promiseResolution.

checkIfNumbersAddToTen(5,5).then(function (arguments) {
console.log('c:'+arguments.c);
console.log('d:'+arguments.d);
console.log('x:'+arguments.x);
},function (failure) {
console.log(failure);
});

Whatever you return from a promise will be wrapped into a promise to be unwrapped at the next .then() stage.

It becomes interesting when you need to return one or more promise(s) alongside one or more synchronous value(s) such as;

Promise.resolve([Promise.resolve(1), Promise.resolve(2), 3, 4])
.then(([p1,p2,n1,n2]) => /* p1 and p2 are still promises */);

In these cases it would be essential to use Promise.all() to get p1 and p2 promises unwrapped at the next .then() stage such as

Promise.resolve(Promise.all([Promise.resolve(1), Promise.resolve(2), 3, 4]))
.then(([p1,p2,n1,n2]) => /* p1 is 1, p2 is 2, n1 is 3 and n2 is 4 */);

You can check Observable represented by Rxjs, lets you return more than one value.

Simply return a tuple:

    async add(dto: TDto): Promise<TDto> {
console.log(`${this.storeName}.add(${dto})`);
return firebase.firestore().collection(this.dtoName)
.withConverter<TDto>(this.converter)
.add(dto)
.then(d => [d.update(this.id, d.id), d.id] as [any, string])
.then(x => this.get(x[1]));
}