Dan
Dan

Reputation: 115

How to make sure $.each pushes all deferreds in jQuery?

I have the following jQuery code:

    myFunc: function(cmd, obj){ 
        var idToExtMap = this. map;
        var requireRes = this.reqInst;
        var deferreds = [];
        var ret = true;
        $.each(idToExtMap[cmd], function(key, ext){
                if(ext.$classIns && ext.$classIns.prepare) {
                    var returnedValue = ext.$classIns.prepare(obj);
                    deferreds.push(returnedValue);
                    $.when(returnedValue).done(function(satisfied){
                        if(ret!==false){
                            ret = satisfied;
                        }
                    });
                } else {
                    requireRes(ext).done(function(){
                        var cls = $.toFunction(ext.$jscls);
                        if(cls) {
                            ext.$classIns = new cls();
                            if(ext.$classIns.prepare){
                                var returnedValue = ext.$classIns.prepare(obj);
                                deferreds.push(returnedValue);
                                $.when(returnedValue).done(function(satisfied){
                                    if(ret!==false){
                                        ret = satisfied;
                                    }
                                });
                            }
                        }
                    });     
                }   
            });

            $.when.apply(null, deferreds).done(function(){
                return ret;
            });
        }

The problem I am having is that $.when.apply is executed before all the deferreds are pushed into deferreds array. How can I make sure that $.when.apply is only executed once all the deferreds are pushed into the deferreds array?

Upvotes: 0

Views: 71

Answers (2)

Roamer-1888
Roamer-1888

Reputation: 19288

The main thing you have to do is ensure that promises are pushed onto the array synchronously. With deferreds.push(...) burried inside a done callback, the push() is asynchronous and the array is guaranteed still to be empty when $.when.apply(...) is executed.

Some other issues can also be fixed :

  • code repetition around ext.$classIns.prepare(obj) can be avoided by rearrangement.
  • the cumbersome outer var ret can be avoided by exploiting promise rejection.

With some other minor tidying, I ended up with this (untested) :

myFunc: function(cmd, obj) {
    var requireRes = this.reqInst;
    var promises = $.map(this.map[cmd], function(key, ext) {
        var p; // p for promise
        if(ext.$classIns) {
            p = $.when(ext.$classIns);
        } else {
            p = requireRes(ext).then(function() {
                var cls = $.toFunction(ext.$jscls);
                if(cls) {
                    ext.$classIns = new cls();
                }
                return ext.$classIns || null;
            });
        }

        /* At this point, `p` is a promise resolved with either a previously created `cls()` object or a freshly created one */

        // As we are inside `$.map(...)`, `return p.then(...)` causes the promise delivered by `p.then(...)` to be pushed onto the `promises` array.
        return p.then(function($classIns) {
            if($classIns && $classIns.prepare) {
                return $classIns.prepare(obj).then(function(satisfied) {
                    if(!satisfied) {
                        // Here, returning a rejected promise is equivalent to setting the original `ret` irrevocably to `false`.
                        return $.Deferred().reject(new Error(".prepare() not satisfied"));
                    }
                });
            } else {
                // You may want also to reject if `$classIns` or `$classIns.prepare` doesn't exist.
                // If not, then delete this else{} clause.
                return $.Deferred().reject(new Error(".prepare() could not be called"));
            }
        });
    });

    /* At this point, `promises` is an array of promises - some or all resolved, some or all pending */

    // Here, instead of the boolean `ret`, you can exploit the joined promise's success/error paths :
    // * success path is equivalent to ret == true.
    // * error path is equivalent to ret == false, or any unpredicted error.
    return $.when.apply(null, promises).then(function() {
        // Success.
        // Do whatever is necessary here or in `myFunc().then(function() {...})`.
    }, function(e) {
        // An error occurred.
        // Do whatever is necessary here or in `myFunc().then(null, function() {...})`.
        console.error(e); //for example
    });
}

Comments should explain what's going on.

Upvotes: 1

jfriend00
jfriend00

Reputation: 707606

Move $.when.apply() outside/after the .each() loop so you don't call it until after you're done building the deferreds array.

You have other issues though. I looks like you're trying to return a value from $.when().done(). As you have your code structured, that won't do anything. You will need to either return a promise from your function or add a callback to your function that you can then call when you have your final result. This is because your operations are asynchronous and will finish long after your main function returns.

Upvotes: 0

Related Questions