Reputation: 183
Almost all of the functions in my program have some sort of asynchronous call, but they all rely on some previous function's results. Because of that, I've hard-coded the next function call into each individual one as such:
function getStuff() {
$.ajax({
...
success: function(results) {
// other functions involving results
getMoreStuff(results);
}
});
}
function getMoreStuff(results) {
$.ajax({
...
success: function(moreResults) {
// other functions involving moreResults
doSomethingWithStuff(moreResults);
}
);
}
And so on. It's a large chain where each function calls the next. While this works within the program, it makes each function useless individually.
I'm a bit lost on how to avoid this problem. I couldn't figure out how to use general callback functions, because when I make the function calls, it ends up like this (using the functions above):
getStuff(function() {
getMoreStuff(results, doSomethingWithStuff);
};
But then 'results' hasn't been defined yet.
The solution seems obvious, I'm just being a bit dense about it. Sorry!
Upvotes: 11
Views: 673
Reputation: 9374
The solution is very simple. You have to use Publish–subscribe
pattern.
The easiest implementation with jQuery:
$('body').trigger('joined-game', [game_id, response]);
First argument is the event name which you're publishing, second argument is the data array.
The best practise is to trigger event on the most specific DOM element, but if you subscribe the same event on multiple pages, and aren't sure if DOM element is present on all pages you can trigger it on body
or some "dump/synthetic" not visible DOM element always present on all pages.
$("body").on('joined-game', function(event, game_id, response){
//...
});
Then you subscribe event you want to utilize. Remember that besides your data, first argument is always event.
Another advantage of this solution is that you can split your code to several files.
More details: http://webility.pl/en/blog/entry/chaining-javascript-functions-without-dependecy-hell
Upvotes: 2
Reputation: 1074335
You have a couple of choices. You can have your code using those functions look like this, using callbacks:
getStuff(function(results) {
getMoreStuff(results, doSomethingWithStuff);
});
or like this, using jQuery's Deferred
and Promise
objects:
getStuff().then(getMoreStuff).then(doSomethingWithStuff):
Have both getStuff
and getMoreStuff
accept an argument that is a callback to call when they're done, e.g.:
function getStuff(callback) {
// ^------------------------------ callback argument
$.ajax({
...
success: function(results) {
// other functions involving results
callback(results);
// ^------------------------------------ use the callback arg
}
});
}
...and similarly for getMoreStuff
.
Deferred
and Promise
jQuery's ajax
function integrates with its Deferred
and Promise
features. You can just add return
to your existing functions to make that work, e.g.:
function getStuff(callback) {
return $.ajax({
...
});
}
(Note: No need for the success
callback.)
Then this code:
getStuff().then(getMoreStuff).then(doSomethingWithStuff);
does this:
getStuff
starts its ajax
call and returns the Promise
that call creates.
When that ajax
call completes and resolves the promise, getMoreStuff
is called with the results of the ajax
call as its first argument. It starts its ajax
call.
When getMoreStuff
's ajax
call completes, doSomethingWithStuff
is called with the results of that call (the one in getMoreStuff
).
It's important to use then
, not done
, in order to get the correct results passed on at each stage. (If you use done
, both getMoreStuff
and doSomethingWithStuff
will see the results of getStuff
's ajax
call.)
Here's a full example using ajax
:
Fiddle | Alternate Fiddle with the ajax
calls taking one second each (makes it easier to see what's happening)
function getStuff() {
display("getStuff starting ajax")
return $.ajax({
url: "/echo/json/",
type: "POST",
data: {json: '{"message": "data from first request"}'},
dataType: "json"
});
}
function getMoreStuff(results) {
display("getMoreStuff got " + results.message + ", starting ajax");
return $.ajax({
url: "/echo/json/",
type: "POST",
data: {json: '{"message": "data from second request"}'},
dataType: "json"
});
}
function doSomethingWithStuff(results) {
display("doSomethingWithStuff got " + results.message);
}
getStuff().then(getMoreStuff).then(doSomethingWithStuff);
function display(msg) {
var p = document.createElement('p');
p.innerHTML = String(msg);
document.body.appendChild(p);
}
Output:
getStuff starting ajax getMoreStuff got data from first request, starting ajax doSomethingWithStuff got data from second request
You don't need to be using ajax
to get the benefit of this, you can use your own Deferred
and Promise
objects, which lets you write chains like this:
one().then(two).then(three);
...for any situation where you may have asynchronous completions.
Here's a non-ajax
example:
function one() {
var d = new $.Deferred();
display("one running");
setTimeout(function() {
display("one resolving");
d.resolve("one");
}, 1000);
return d.promise();
}
function two(arg) {
var d = new $.Deferred();
display("Two: Got '" + arg + "'");
setTimeout(function() {
display("two resolving");
d.resolve("two");
}, 500);
return d.promise();
}
function three(arg) {
var d = new $.Deferred();
display("Three: Got '" + arg + "'");
setTimeout(function() {
display("three resolving");
d.resolve("three");
}, 500);
return d.promise();
}
one().then(two).then(three);
function display(msg) {
var p = document.createElement('p');
p.innerHTML = String(msg);
document.body.appendChild(p);
}
Output:
one running one resolving Two: Got 'one' two resolving Three: Got 'two' three resolving
These two (the ajax
example and the non-ajax
example) can be combined when necessary. For instance, if we take getStuff
from the ajax
example and we decide we have to do some processing on the data before we hand it off to getMoreStuff
, we'd change it like this: Fiddle
function getStuff() {
// Create our own Deferred
var d = new $.Deferred();
display("getStuff starting ajax")
$.ajax({
url: "/echo/json/",
type: "POST",
data: {json: '{"message": "data from first request"}', delay: 1},
dataType: "json",
success: function(data) {
// Modify the data
data.message = "MODIFIED " + data.message;
// Resolve with the modified data
d.resolve(data);
}
});
return d;
}
Note that how we use that didn't change:
getStuff().then(getMoreStuff).then(doSomethingWithStuff);
All that changed was within getStuff
.
This is one of the great things about the whole "promise" concept (which isn't at all specific to jQuery, but jQuery gives us handy versions to use), it's fantastic for decoupling things.
Upvotes: 11
Reputation: 388316
Try
function getStuff() {
return $.ajax({
...
success: function(results) {
// other functions involving results
}
});
}
function getMoreStuff(results) {
return $.ajax({
...
success: function(moreResults) {
// other functions involving moreResults
}
);
}
Then
getStufff().done(function(){
getMoreStuff().done(doSomethingWithStuff)
})
etc
Upvotes: 5
Reputation: 93561
Pass callbacks that accept a parameter:
function getStuff( callback ) {
$.ajax({
...
success: function(results) {
// callback with result
callback(results);
}
});
}
function getMoreStuff(results, callback) {
$.ajax({
...
success: function(moreResults) {
// callback with result
callback(moreResults);
}
);
}
function doSomethingWithStuff(results, callback) {
// process results via some means not described herein :)
if (callback){
// callback yet again with results, but only if callback provided this time
callback(stillMoreResults);
}
}
Then use with something like:
getStuff(function(results) {
getMoreStuff(results, function(moreresults){
doSomethingWithStuff(moreresults);
});
};
This pattern is generally useful for any async operations. It is not specific to Ajax calls (I used it to create a complete animated board game in JQuery).
Upvotes: 4