It's 2018 so there are multiple, nice ways of doing this;
- You can use
Promises
, $.ajax
actually returns one; and async/await
to perform XHR requests serially.
- You can keep your callback-style code and use a small utility function to abstract the async iteration in a nice readable way that you can reuse over and over.
I'll cover both cases.
Since jQuery 1.5, $.ajax
returns a Promise
. So if you're using a modern browser you can just await
it.
This is by far most elegant and terse way since the code looks like synchronous code hence it's far more readable. Be aware that while the code looks synchronous, it's in fact non-blocking.
const getPosts = async (pages) => {
const posts = []
for (const page of pages) {
const post = await $.ajax({
url: 'https://jsonplaceholder.typicode.com/posts/' + page
})
posts.push(post)
}
return posts
}
getPosts([1, 2, 3, 4, 5]).then(posts => {
console.log(posts)
}).catch(err => {
console.error(err)
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Async Iteration with callbacks
This is the "traditional" way of doing asynchronous operations, that uses callbacks. This style doesn't require a modern browser at all since at it's base level it just passes around functions to achieve non-blocking behaviour.
However, this type of code is far harder to work with; You need to rely on utility functions that wrap common operations (looping, mapping etc) to effectively work with such code.
I've written a small utility function that let's you:
- Iterate over the elements of an Array.
- Specify a callback function that get's called for each iteration
- This function get's called on each iteration. The currently-iterated over Array element is passed to it, together with a
next
argument that you need to call in order to proceed to the next iteration.
- Calling the
next
of this callback function pushes the result into a final result
Array, and proceeds to the next iteration.
- Specify a final callback function that get's called when all the iterations have finished.
If I'm not mistaken, this is identical in operation to the async.mapSeries method of the popular async module.
async.mapSeries
:
In the following example, I'm passing an Array
of posts to fetch from a REST API.
When all the iterations are complete, the posts
argument in the final callback contains an Array
with 5 posts.
It takes advantage of error-first callbacks, a common pattern to gracefully propagate errors up the callback chain if something goes awry in your async operations.
// async.mapSeries utility Function
const async = {
mapSeries: function(arr, onIteration, onDone, { i = 0, acc = [] } = {}) {
arr.length ?
onIteration(arr[i], (err, result) => {
if (err) return onDone(err)
acc.push(result)
acc.length < arr.length ?
this.mapSeries(arr, onIteration, onDone, {
i: ++i, acc
}) : onDone(null, acc)
})
: onDone(null, arr)
}
}
// Usage
async.mapSeries([1, 2, 3, 4, 5], (page, next) => {
$.ajax({
url: 'https://jsonplaceholder.typicode.com/posts/' + page,
success: response => {
next(null, response)
},
error: (XMLHttpRequest, textStatus, err) => {
next(err)
}
})
}, (err, posts) => {
if (err) return console.error('Error:', err)
console.log(posts)
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>