-->

Wait for multiple promises to be rejected

2020-04-10 03:15发布

问题:

Doing some testing, I need to wait for a couple of promises to be rejected.

I know that I can use jQuery.when() to wait for several promises tu be resolved. But that method reject the master promise as soon one of the promises fails.

I know that all my promises will fail, but I need to wait for it anyway. How can I do it?

In a kind of pseudo-code what I want to do is:

var promise1 = connection.doCall();
var promise2 = connection.doCall();

$.when([promise1, promise2]).allOfThemFail(function() {
    assertThatSomeProcessWasDoneOnlyOnce()
});

回答1:

Using the Bluebird promise library, you can use Promise.settle() which waits for all the promises to be settled (e.g. fulfilled or rejected) and then you can simply query the list to see if they were all rejected.

var p1 = connection.doCall();
var p2 = connection.doCall();
Promise.settle([p1, p2]).then(function(results) {
    var allRejected = results.every(function(item) {
        return item.isRejected();
    });
    // act on allRejected here
});

Here's a jQuery-specific version of settle():

(function() {    

    function isPromise(p) {
        return p && (typeof p === "object" || typeof p === "function") && typeof p.then === "function";
    }

    function wrapInPromise(p) {
        if (!isPromise(p)) {
            p = $.Deferred().resolve(p);
        }
        return p;
    }

    function PromiseInspection(fulfilled, val) {
        return {
            isFulfilled: function() {
                return fulfilled;
            }, isRejected: function() {
                return !fulfilled;
            }, isPending: function() {
                // PromiseInspection objects created here are never pending
                return false;
            }, value: function() {
                if (!fulfilled) {
                    throw new Error("Can't call .value() on a promise that is not fulfilled");
                }
                return val;
            }, reason: function() {
                if (fulfilled) {
                    throw new Error("Can't call .reason() on a promise that is fulfilled");
                }
                return val;
            }
        };
    }

    // pass either multiple promises as separate arguments or an array of promises
    $.settle = function(p1) {
        var args;
        if (Array.isArray(p1)) {
              args = p1;
        } else {
            args = Array.prototype.slice.call(arguments);
        }

        return $.when.apply($, args.map(function(p) {
            // make sure p is a promise (it could be just a value)
            p = wrapInPromise(p);
            // Now we know for sure that p is a promise
            // Make sure that the returned promise here is always resolved with a PromiseInspection object, never rejected
            return p.then(function(val) {
                return new PromiseInspection(true, val);
            }, function(reason) {
                // convert rejected promise into resolved promise by returning a resolved promised
                // One could just return the promiseInspection object directly if jQuery was
                // Promise spec compliant, but jQuery 1.x and 2.x are not so we have to take this extra step
                return wrapInPromise(new PromiseInspection(false, reason));
            });
        })).then(function() {
              // return an array of results which is just more convenient to work with
              // than the separate arguments that $.when() would normally return
            return Array.prototype.slice.call(arguments);
        });
    }

})();

And, you would use this similarly to the Bluebird solution:

var p1 = connection.doCall();
var p2 = connection.doCall();
$.settle([p1, p2]).then(function(results) {
    var allRejected = results.every(function(item) {
        return item.isRejected();
    });
    // act on allRejected here
});

And, here's a settle() type function build from scratch using standard ES6 promises. It returns a single promise that is resolved when all the other promises have finished (regardless of their final state). The resolved result of this uber promise is an array of PromiseInspection objects where you can query isFulfilled() or isRejected() and can obtain the .value() or .reason().

Promise.isPromise = function(p) {
    return p && (typeof p === "object" || typeof p === "function") && typeof p.then === "function";
}

// ES6 version of settle
Promise.settle = function(promises) {
    function PromiseInspection(fulfilled, val) {
        return {
            isFulfilled: function() {
                return fulfilled;
            }, isRejected: function() {
                return !fulfilled;
            }, isPending: function() {
                // PromiseInspection objects created here are never pending
                return false;
            }, value: function() {
                if (!fulfilled) {
                    throw new Error("Can't call .value() on a promise that is not fulfilled");
                }
                return val;
            }, reason: function() {
                if (fulfilled) {
                    throw new Error("Can't call .reason() on a promise that is fulfilled");
                }
                return val;
            }
        };
    }

    return Promise.all(promises.map(function(p) {
        // make sure any values are wrapped in a promise
        if (!Promise.isPromise(p)) {
            p = Promise.resolve(p);
        }
        return p.then(function(val) {
            return new PromiseInspection(true, val);
        }, function(err) {
            return new PromiseInspection(false, val);
        });
    }));
}

And, you would use this similarly as above:

var p1 = connection.doCall();
var p2 = connection.doCall();
Promise.settle([p1, p2]).then(function(results) {
    var allRejected = results.every(function(item) {
        return item.isRejected();
    });
    // act on allRejected here
});


回答2:

I don't think there is a built in function for that, but you can easily implement

var promise1 = connection.doCall();
var promise2 = connection.doCall();

allOfThemFail([promise1, promise2]).always(function () {
    assertThatSomeProcessWasDoneOnlyOnce()
});


function allOfThemFail(array) {
    var count = 0;
    len = array.length, failed = false, deferred = $.Deferred();

    $.each(array, function (i, item) {
        item.fail(fail).always(always);
    })


    function always() {
        if (++count == len) {
            if (failed) {
                deferred.reject();
            } else {
                deferred.resolve();
            }
        }
    }

    function fail() {
        failed = true;
    }

    return deferred.promise();
}