How do you attach callbacks to the deferred object

2019-05-22 13:27发布

问题:

I have a chain of ajax requests that support a series of cascading drop down select lists. When you select a value in the 1st drop down, a request is fired to populate the 2nd, when that is complete (and the drop down filled), the next request fires to populate the 3rd drop down, and so on down the line.

There are some variations to the way these request chains are formed, so I was hoping to assemble the requests using the jQuery Deferred objects.

I see how I can chain the 2nd request to the first, but I don't see how I can chain the third request to the 2nd.

function Step1() { return $.ajax(<foo>);}
function Step2() { return $.ajax(<foo>);}
function Step3() { return $.ajax(<foo>);}

$(function() {
   Step1().then(Step2).then(Step3);
});

The intent is that Step3 is triggered when Step2 is resolved, but the deferred object returned by .then(Step2) is from Step1, so Step3 is added as a callback to Step1.

I think it is clearer what I'm trying to do if you please see this jsFiddle sample. Edit:Here is the same script with a delay added to the second call to make it more obvious.

回答1:

$(function() {
    $.when(Step1).then(function() {
         $.when(Step2).then(Step3);
    });
});

For error handling I recommend you rewrite Stepn to :

function Stepn() { 
    return $.ajax(<foo>).fail(function() {
         // handle failure
    });
}

Using callbacks in this format allows you to do what you want. if you have more then 5 steps the indenting becomes a mess and it might be worthwhile to build a queue for this.

Here's a live example

var Queue = function() {
    var q = [];
    var that = this;

    // If items in queue then run them.
    function moveNext() {
        if (q.length > 0) {
            that.runItem();
        }
    }

    // run first item in queue
    this.runItem = function() {
        // get item
        var item = q.shift();
        // when deferred object then run then ...
        $.when(item.item).then([item.options.done, function() {
            // item finished, move to next.
            moveNext();
        }], [item.options.fail, function() {
            // if run item always then move next on failure.
            if (item.options.always) {
                moveNext();
            }
        }]);
    };

    this.add = function(def, options) {
        // if array then call add on each item in array
        if ($.isArray(def)) {
            for (var d in def) {
                this.add(d, options);
            }
            // return as we are done.
            return this;
        }
        // push item onto array
        q.push({
            item: def,
            options: options
        });
        // if items & not delay then run item.
        if (q.length === 1 && !options.delay) {
            this.runItem();
        }
        // enable jQuery style chaining \o/
        return this;
    };
};

Queue.add([def, def, ...], options) Adds a defferred item or an array of deferred items to the queue. Can be used with either a single deferred item or an array. The options map is as follows

{
    "delay" : Boolean, // if true do not run the item in the queue after appending it.
    "done" : Function, // optional done call back
    "fail" : Function, // optional fail call back
    "always": Boolean // if true run the next item in the queue even if this item fails.
}

Queue.runItem, a function that runs the next item in the queue. Called internally, possible to be used manually in concatenation with the delay property.



回答2:

I started wrestling with this recently (see my question here), inspired by a blog series by James Coglan.

After messing around with 'monads' for a while, I came back to wishing that it were possible to 'chain' deferred objects together. The problem is that "done" returns the same deferred object, instead of a new one.

I browsed the jquery code for a while, and figured there was no way I could inject anything into the Deferred or _Deferred code. However, it is possible to inject our own object as a parameter to the promise() function. So if we create a function which will generate a chainable promise for us...

var Chainable = function Chainable() {
     return {
         chain : function(next) { //next: another function which returns Deferred
                 var newDef = $.Deferred(); //we resolve this when next is done
                 //next line: call next with (a||null) for method-tolerance
                 this.done(function(a) { next(a||null).done(newDef.resolve); });
                 return newDef.promise(Chainable());
         }
     };
 }

...we can then use it to pimp our style:

var asyncMessage = function(msg) {
    var dfd = new jQuery.Deferred();
    setTimeout(function() { dfd.resolve(msg); }, 1000);
    return dfd.promise(Chainable());
};

asyncMessage("Chained:A")
     .chain(function(m) { return asyncMessage(m + "B"); })
     .chain(function(m) { return asyncMessage(m + "C"); })
     .done(log); // -> outputs "ABC"

See the jsfiddle here for the 'before/after' code samples: http://jsfiddle.net/Benjol/DjrRD/



回答3:

We were fortunate and had some flexibility in the timeline. We ended up using the .pipe() chaining added to deferred objects in jQuery 1.6.

Thanks to everyone for their help!