How to use jQuery's deferred object with custom javascript objects?
I have a standard javascript object whose prototype is extended with a .start()
method taking 2 callbacks as arguments: success
and failure
respectively. This method performs some asynchronous processin开发者_如何学运维g (it's not AJAX) and depending on the result of this processing it invokes either the success or the failure callbacks.
Here's how this could be schematized:
function MyObject() {
}
MyObject.prototype.start = function(successCallback, errorCallback) {
(function(s, e) {
window.setTimeout(function() {
if (Math.random() < 0.8) {
s();
} else {
e();
}
}, 2000);
})(successCallback, errorCallback);
}
It's not really important the exact processing performed inside the method, only that it is asynchronous and non-blocking. I have no control over the point of time when the start method will finish the processing. I also have no control over the prototype and implementation of this method.
What I have control over is the success
and failure
callbacks. It is up to me to provide them.
Now I have an array of those objects:
var arr = [ new MyObject(), new MyObject(), new MyObject() ];
The order of elements in this array is important. I need to trigger the .start()
method on each element of the array consecutively but only once the previous has completed (i.e. the success callback was called). And if an error occurs (the failure callback is called) I want to stop the execution and no longer invoke the .start method on the remaining elements of the array.
I could implement this naively by using a recursive function:
function doProcessing(array, index) {
array[index++].start(function() {
console.log('finished processing the ' + index + ' element');
if (index < array.length) {
doProcessing(array, index);
}
}, function() {
console.log('some error ocurred');
});
}
doProcessing(arr, 0);
This works fine but looking at the jQuery's deferred Object that was introduced in jQuery 1.5 I think that there is a room for improvement of this code. Unfortunately I don't feel very comfortable yet with it and I am trying to learn it.
So my question is is it possible to adapt my naive code and take advantage of this new API and if yes, could you provide me with some pointers?
Here's a jsfiddle with my implementation.
You could do something like this: (jsFiddle)
function MyObject() {
}
MyObject.prototype.start = function(queue) {
var deferred = $.Deferred();
//only execute this when everything else in the queue has finished and succeeded
$.when.apply(jQuery,queue).done(function() {
window.setTimeout(function() {
if (Math.random() < 0.8) {
deferred.resolve();
} else {
deferred.reject();
}
}, 2000);
});
return deferred;
}
var arr = [ new MyObject(), new MyObject(), new MyObject() ];
var queue = new Array();
$.each(arr, function(index, value) {
queue.push(value.start(queue)
.done(function() {
console.log('succeeded ' + index);
})
.fail(function() {
console.log('failed ' + index);
}));
});
Not quite sure wether you would consider this an improvement, though.
When we program, to remember the GRASP principles or guidelines is very important.
http://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
To get High Cohesion and Low Coupling means that our code will be better, more reusable and easier to maintain.
So, the class MyObject mustn't known the queue existance. MyObject will know its own features and methods and anything more.
// Class MyObject
function MyObject(name) {
this.name = name;
}
MyObject.prototype.start = function() {
var deferred = $.Deferred();
var self = this;
setTimeout(function() {
if (Math.random() <= 0.8) {
console.log(self.name + "... ok");
deferred.resolve();
} else {
console.log(self.name + "... fail");
deferred.reject();
}
}, 1000);
return deferred.promise();
}
The main/caller function will know MyObject existance and it will create three instances that they will be executed sequentially.
// Create array of instances
var objectArray = [ new MyObject("A"), new MyObject("B"), new MyObject("C") ];
// Create array of functions to call start function
var functionArray = [];
$.each(objectArray, function(i, obj) {
functionArray.push(
function() {
return obj.start();
}
);
});
// Chain three start calls
$.iterativeWhen(functionArray[0], functionArray[1], functionArray[2])
.done(function() {
console.log("First: Global success");
// Chain three start calls using array
$.iterativeWhen.apply($, functionArray)
.done(function() {
console.log("Second: Global success");
})
.fail(function() {
console.log("Second: Global fail");
});
})
.fail(function() {
console.log("First: Global fail");
});
I have built a plugin for jQuery: iterativeWhen. It works with jQuery 1.8 and later versions.
$.iterativeWhen = function () {
var deferred = $.Deferred();
var promise = deferred.promise();
$.each(arguments, function(i, obj) {
promise = promise.then(function() {
return obj();
});
});
deferred.resolve();
return promise;
};
Jsfiddle here: http://jsfiddle.net/WMBfv/
There's nothing wrong with your implementation. And as we all know, using jQuery isn't always the best method.
I'd do it like this: (without the need to modify the MyObject class..)
function doProcessing(array, index) {
var defer = new $.Deferred();
$.when(defer).then(doProcessing);
array[index++].start(function() {
log('finished processing the ' + index + ' element');
if (index < array.length) {
defer.resolve(array, index);
}
}, function() {
log('some error ocurred => interrupting the process');
});
};
As you can see, there's no real advantage over the plain JavaScript method. :)
Here's my fiddle: http://jsfiddle.net/jwa91/EbWDQ/
精彩评论