JS promises for faster code
In which I use jQuery .when.apply() to process the result of multiple API calls, and make code faster by avoiding setTimeout()
Note: code used below accomplishes things with jQuery that can also be done with ES6 if you are programming in an environment where you may assume your users' browsers are up to date enough to support the latest JavaScript.
The transformation
I am pretty proud of a function I refactored recently for my job at Cornell University Library. The diff can be found on GitHub, but just have a glance at how much the code changed:
This large reduction in number of lines of code was driven by re-thinking the technique for making multiple API requests.
Should we wait for requests to finish?
The function makes calls to three APIs, it does some processing of the results of the API calls, and then it passes the results to the next function. The processing of those API requests can't simply be coded in line after the API calls, because JavaScript will run the processing before the API calls are completed! Something must be done to ensure these things run in the right order.
The code (before refactoring) used the success
and complete
methods to sequence the processing after the API calls. Then, to pass control flow to the next function, it waited with the setTimeout()
method. So, the code simply waited for a fixed time that, in the judgement of the developer, is probably long enough that most or all of the API calls have finished.
There is a big problem with this approach: the time waited might be wrong. If the time is too short, the API calls won't be complete when the code attempts to process them. If the time is too long, the code will be wastefully idle while the API calls have already finished. It would be nice to process the results when, and only when, they are actually ready!
Why not use callbacks?
Maybe the code could sequence things using a callback? After all, the success
and complete
methods are a kind of callback, right? Can't we just pass control flow to the next function by the same technique?
That would work if there were only one API call, but there are three. Whichever one passes the callback may not be the last one to execute. We need to wait for all of them.
Promises, promises
We can use promises to sequence our code. jQuery provides a tricky way to await not just one promise, but an indefinite number of them. It looks like $.when.apply($, ...) followed by .done( ... ).
The trick works because:
$.when
takes any number of promises as parameters and resolves when all the promises have resolved.apply()
converts an array of promises to parameters.done()
is a callback function; the code within it is deferred until$.when
resolves
The below code shows how I used this technique:
function ajaxRequestsForSuggestedSearches(q) {var ajaxParametersList = [
{
url: 'https://lookup.ld4l.org/authorities/search/...',
dataType: 'json'
},
{
url: 'http://lookup.dbpedia.org/api/search/...',
dataType: 'json'
},
{
url: 'https://www.wikidata.org/w/api.php?action=...',
dataType: 'jsonp'
}
];
// return an array of Ajax promises
return ajaxParametersList.map(p => $.ajax(p));
}
function gatherSuggestions(q) {
// get array of Ajax request promises
var ajaxRequests = ajaxRequestsForSuggestedSearches(q);
// run each request in the array
var whenRequests = $.when.apply($, ajaxRequests);
// when done running, process responses
whenRequests.done(function(ld4l, dbpedia, wikidata){
// ... do stuff to process request results ...
// pass the output of processing on to next function
checkSuggestions(processedData);
})
}
The first function in the above code starts with an array of info that will be used for network connections. The last line of that first function sets up an $.ajax()
promise for each connection.
The second function in the code takes that array of promises and uses $.when
to prepare to do further work when each promise is resolved. The method .done()
is used to specify that further work.
I think it is bad/weird to put large chunks of code inside the level of indentation that .done()
causes, so my program's control flow passes pretty quickly to another function (named checkSuggestions()
in this case).
But, that's not the only reason to break your code into functions of manageable size! I've written more about that here
Random note: I experimented with processing the requests with the dataFilter
option built in to &.ajax()
, instead of processing them within done
.
It turns out that dataFilter
does not work for JSONP requests! Thanks to David Flanagan's JavaScript book for that insight!