问题
It's possible this should be on code review, but here we go!
I have a fairly large application with a lot of ajax calls. I started using Q for some async stuff, and figured I would wrap the ajax calls in Q to ensure all async methods have the same signature.
I'm using a global facade method, so my ajax calls look like:
App.ajax( config ).then( doWhatever );
with App.ajax looking something like this:
ajax: function( config ){
var ajaxReturn = $.ajax( config );
ajaxReturn.error(function( xhr ){
// some custom error handling
});
return ajaxReturn;
}
I modified App.ajax to look like this:
ajax: function( config ){
var dfd = Q.defer();
var ajaxReturn = $.ajax( config );
ajaxReturn.done(function( results, status, xhr ){
delete xhr.then;
dfd.resolve( results );
});
ajaxReturn.error(function( xhr ){
// some custom error handling
dfd.reject( "some message generated by error handling" );
});
return dfd.promise;
}
This works, 0 changes needed on individual ajax calls themselves, but it caused the Ajax calls that cared about the "status" and "xhr" parts of the $.ajax return to stop working.
I've read Q's docs about coming from jQuery, and it basically just suggests "you should treat promise-returning functions like normal synchronous functions, in that you should assume they only return a single object"
Now, I didn't want to have to refactor all the calls that care about the xhr object to either take an object or use spread instead of then/done. So I added this code right before returning the Q promise:
dfd.promise.then = function( callback ){
dfd.promise.then = Q().then;
return dfd.promise.spread( callback );
};
and changed my resolve line to:
dfd.resolve( [ results, status, xhr ] );
and now App.ajax looks like this:
ajax: function( config ){
var dfd = Q.defer();
var ajaxReturn = $.ajax( config );
ajaxReturn.done(function( results, status, xhr ){
delete xhr.then;
dfd.resolve( [ results, status, xhr ] );
});
ajaxReturn.error(function( xhr ){
// some custom error handling
dfd.reject( "some message generated by error handling" );
});
dfd.promise.then = function( callback ){
dfd.promise.then = Q().then;
return dfd.promise.spread( callback );
};
return dfd.promise;
}
So this is over-riding this specific Deferred's then function with a wrapper function that will reset then to the "real" then and use spread instead for this call. This results in my App ajax calls being able to retain their original signature, but still be a Q promise through and through.
This seems to work fine, and that's great.
now, finally, for the questions But is this advisable? I understand there's minor performance implications in unnecessarily creating the extra Promise in the custom "then" method. I don't care about that. I'm more wondering if there's some gotcha that hasn't bit me yet where this is strictly a very bad idea.
Edit:
There are some problems with the above block. This is the most up to date, most correct block of code. The promise's then
can't just be reset to be the 'real' then
, because subsequent calls to the initial promise don't get the proper spread result, so we have to reset the then method back to the over-ridden one that calls spread before returning the new spread promise.
ajax: function( config ){
var dfd = Q.defer();
var ajaxReturn = $.ajax( config );
ajaxReturn.done(function( results, status, xhr ){
delete xhr.then;
dfd.resolve( [ results, status, xhr ] );
});
ajaxReturn.error(function( xhr ){
// some custom error handling
dfd.reject( "some message generated by error handling" );
});
var cachedThen = dfd.promise.then;
dfd.promise.then = function overrideThen( fulfilled, rejected ){
dfd.promise.then = cachedThen;
var spreadPromise = dfd.promise.spread( fulfilled, rejected );
dfd.promise.then = overrideThen;
return spreadPromise;
};
return dfd.promise;
}
回答1:
You're overthinking it. Q is designed to interoperate with jQuery promises.
If you want to convert a function to return a Q promise - just wrap it with Q()
:
myAjaxMethod(); // returns a jQuery promise
Q(myAjaxMethod()); // returns a Q promise that wraps the jQuery one
So for example - your ajax
method can be:
function ajax(config){
return Q($.ajax(config));
}
Which is shorter and less error prone.
回答2:
It seems like for the sake of one good idea (wrap jQuery's promises in Q promises) you've come up with two or three bad ideas. That stuff you're doing with dfd.promise.then
is complete insanity, and it doesn't work. Observe:
var p = App.ajax({ url: "..." });
p.then(function (data) {
// ok, no problem
console.log(data);
});
// if p is not hopelessly broken at this point, the following statement
// should have exactly the same outcome as the previous one
p.then(function (data) {
// Logs "undefined"! You REPLACED p's .then() method with
// one from an empty promise in the last statement!
console.log(data);
});
Even if you find a way around the particular issue above, it's not wise to do this sort of thing without having a deep understanding of the implications. I have not read the Q library's source in much detail, but it would not surprise me if there are some internal dependencies that rely on the assumption that the then
method isn't getting swapped out for something else.
As Benjamin Gruenbaum says, don't overthink it. Don't try to turn promises into something they're not in an attempt to avoid updating your code. Promises/A+ compliant promises only pass one argument into their .then()
handlers. By trying to circumvent that, you're completely undermining the good idea you started off with.
回答3:
The best I can offer is not dramatically different from some of the stuff in the question, however is is more economically mechanised and exploits Q's ability to coerce a jQuery promise, as recommended by Benjamin G.
First a utility function :
function unspread() {
return Array.prototype.slice.call(arguments);
}
Now in your ajax()
function you can use the unspread()
utility to bundle jQuery's multiple args :
function ajax(options) {
return Q($.ajax(options).then(unspread, unspread));
}
Success and failure handlers unfortunately have to be dissimilar due to the nature of Q's .spread()
method, which quite correctly spreads only on success, not on failure.
function foo(options) {
return ajax(options).spread(function(response, textStatus, xhr) {
console.dir(response);
console.log(textStatus);
console.dir(xhr);
return response;//or rebundle the args into an array/object
}, function(arr) {
console.dir(arr[0]);//xhr
console.log(arr[1]);//textStatus
console.dir(arr[2]);//ErrorThrown
throw arr;//or any of its elements
});
}
If you really wanted named error arguments in Q, then here's a decidedly messy (and untested) approach :
function foo(options) {
return ajax(options).spread(function(response, textStatus, xhr) {
console.dir(response);
console.log(textStatus);
console.dir(xhr);
return response;
}, function(err) {
if($.isArray(err)) {
//it's a $.ajax error array
return err; //put the error array on the success path so it can be spread
} else {
throw err;//maybe it's an unbundeled error
}
}).spread(function(xhr, textStatus, ErrorThrown) {
if(arguments.length == 3) {//test because the genuine success path will lead you here too.
console.dir(xhr);
console.log(textStatus);
console.dir(ErrorThrown);
}
});
}
But, even if you could get that to work, it's rather extreme just to obtain named args.
I'm sure a "Q.superSpread()" method could be written to do the job. I gave it 10 minutes and decided it was not trivial, equally extreme and probably conceptually unsound.
来源:https://stackoverflow.com/questions/27903781/wrapping-jquery-ajax-behind-a-facade-in-q-js-without-refactoring