Can you avoid nesting altogether with Promises? [d

2020-07-13 13:08发布

问题:

From what I understand one of the main selling points for Promises is the ability to write flat code (or, flatter than callback hell).

Though it seems that in many cases we need to nest promises, in order to use closure. For example (from q's docs, though I use Bluebird):

function authenticate() {
    return getUsername()
        .then(function (username) {
            return getUser(username);
        })
        // chained because we will not need the user name in the next event
        .then(function (user) {
            return getPassword()
                // nested because we need both user and password next
                .then(function (password) {
                    if (user.passwordHash !== hash(password)) {
                        throw new Error("Can't authenticate");
                    }
                });
        });
}

Is there a cleaner way to do this, without nesting?

EDIT: I've managed to clean up this specific example using .all, but there are more complex cases where I don't think it can be done:

function authenticate() {
    return Promise.all([
        getUsername().then(getUser),
        getPassword()
    ]).spread(function (user, password) {
        if (user.passwordHash !== hash(password)) {
            throw new Error('Can\'t authenticate');
        }
    });
}

回答1:

Yes, you can always flatten a promise chain with Promise.all (shorthand through Promise.join in Bluebird) by using promises for the proxies they are. After all - promises abstract values, you can always unwrap a promise as late as you want and have other variables depend on it.

Whether or not it's more readable is another debate:

foo().then(function(a){
     return bar(a).then(function(b){
          return g(a,b); // "needed" to nest because I wanted `a`
     });
});

Can be written as:

var a = foo();
var b = a.then(bar);
Promise.join(a, b, function(a,b){
    return g(a, b); // alternatively, res could have been `Promise.join(a,b, g)`
});

So generally - you can always avoid nesting but a lot of time you might not want to.

In your case, this can even be:

function authenticate() {
    var userPass = Promise.all([ getUsername().then(getUser), getPassword()]);
    var passHash = userPass.get(0).get("passwordHash");
    var newHash = userPass.get(1).then(hash);     
    var equal = Promise.join(userHash, newHash, function(a, b){ return a !==b });
    return equal.then(function(val){ if(!val) throw new Error("..."); });
}

Flatter? Sure. Better? That's a whole other question. If you have a nested for loop, you might want to keep it a nested for loop and nest rather than hack around that option and use a single loop.