Embrace the asynchronous behavior of JavaScript! While some
asynchronous actions offer synchronous equivalents (so does "Ajax"),
it's usually discouraged to use them, particularly in a browser context.
Why is it not good do you ask?
JavaScript goes in the UI thread of the browser and any long
running process will lock the UI, creating it unresponsive. Moreover, there is
an upper limit on the performance time for JavaScript and the browser will ask
the user whether to continue the implementation or not.
All of this is really not good user experience. The user
won't be able to tell whether all is working fine or not. Also, the effect will
be not good for users with a slow connection.
In the following we will look at three different answers
that are all building on top of each other:
Promises with async/await (ES2017+, offered in older
browsers if you use a transpiler or regenerator)
Callbacks (popular in node)
Promises with then() (ES2015+, offered in older browsers if
you use one of the many promise libraries)
All the three are offered
in present browsers, and node 7+.
ES2017+: Promises with async/await
The ECMAScript version released in 2017 announced
syntax-level support for asynchronous functions. With the advantage of async
and await, you can write asynchronous in a "synchronous style". The
code is still asynchronous, but it's easier to read/understand.
async/await forms on top of promises: an async function
always returns a promise. await "unpacks" a promise and either result
in the value the promise was fixed with or throws an error if the promise was forbidden.
Key: You can only use await inside an async function. At present,
top-level await isn't yet supported, so you may have to make an async IIFE
(Immediately Invoked Function Expression) to start an async context.
You can see more about async and await on MDN.
Here is an illustration that forms on top of delay above:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't stated as `async` because it already
returns a promise
function delay() {
// `delay` returns a
promise
return new
Promise(function(resolve, reject) {
// Only `delay` is
able to solve or reject the promise
setTimeout(function() {
resolve(42); //
After three seconds, solve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of
book IDs of the current user
var bookIDs =
await superagent.get('/user/books');
// wait for 3
seconds (just for the sake of this example)
await delay();
// GET information
about each book
return await
superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the anticipated
promises was rejected, this catch block
// would catch the
rejection cause
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await
getAllBooks();
console.log(books);
})();
Existing browser and node versions support async/await. You
can also support older environments by altering your code to ES5 with the assistance
of regenerator (or tools that use regenerator, such as Babel).
Let functions accept callbacks
A callback is basically a function passed to a different
function. That other function can call the function passed at whatever time it
is ready. In the situation of an asynchronous process, the callback will be
called at whatever time the asynchronous process is completed. Typically, the
result is passed to the callback.
In the illustration of the question, you can make foo accept
a callback and use it as success callback. So this
var result = foo();
// Code that depends on 'result'
becomes
foo(function(result) {
// Code that
depends on 'result'
});
Here we state the function "inline" but you can
pass any function reference:
function myCallback(result) {
// Code that be
subject to 'result'
}
foo(myCallback);
foo itself is defined as follows:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback will denote to the function we pass to foo when we
call it and we basically pass it on to success. I.e. once the Ajax request is effective,
$.ajax will call callback and pass the response to the callback (which can be stated
with the outcome, meanwhile this is how we defined the callback).
You can also process the response beforehand passing it to
the callback:
function foo(callback) {
$.ajax({
// ...
success:
function(response) {
// For
example, filter the response
callback(filtered_response);
}
});
}
It's easier to write code using callbacks than it might appear.
After all, JavaScript in the browser is deeply event-driven (DOM events). Getting
the Ajax response is nothing else but an event.
Complications might arise when you have to work with
third-party code, but most difficulties can be resolved by just thinking
through the application flow.
ES2015+: Promises with then()
The Promise API is a fresh feature of ECMAScript 6 (ES2015),
but it has decent browser support now. There are also numerous libraries which
implement the normal Promises API and provide added methods to simplify the use
and composition of asynchronous functions (e.g. bluebird).
Promises are containers for forthcoming values. When the
promise gets the value (it is resolved) or when it is void (rejected), it informs
all of its "listeners" who want to access this value.
The benefit over basic callbacks is that they let you to
decouple your code and they are easier to compose.
Here is a simple illustration of using a promise:
function delay() {
// `delay` returns a
promise
return new
Promise(function(resolve, reject) {
// Only `delay` is
capable to solve or reject the promise
setTimeout(function() {
resolve(42); //
After three seconds, solve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) {
// `delay` returns a promise
console.log(v); //
Log the value once it is solved
})
.catch(function(v) {
// Or do something
else if it is rejected
// (it would not occur
in this illustration, as `reject` is not called).
});
Given to our Ajax call we could use promises like this:
function ajax(url) {
return new
Promise(function(resolve, reject) {
var xhr = new
XMLHttpRequest();
xhr.onload =
function() {
resolve(this.responseText);
};
xhr.onerror =
reject;
xhr.open('GET',
url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending
on result
})
.catch(function() {
// An error
occurred
});
Describing all the advantages that promise offer is beyond
the scope of this answer, but if you write new code, you should seriously
consider them. They deliver a great abstraction and separation of your code.
More info about promises: HTML5 rocks - JavaScript Promises
Side note: jQuery's deferred objects
Deferred objects are jQuery's norm application of promises
(before the Promise API was uniform). They act nearly like promises but picture
a somewhat dissimilar API.
Every Ajax method of jQuery now returns a "deferred
object" (essentially a promise of a deferred object) which you can just
return from your function:
function ajax() {
return
$.ajax(...);
}
ajax().done(function(result) {
// Code depending
on result
}).fail(function() {
// An error
occurred
});
Side note: Promise gotchas
Keep in notice that promises and deferred objects are just
containers for upcoming values, they are not the value itself. For instance, assume
you had the following:
function checkPassword() {
return $.ajax({
url:
'/password',
data: {
username:
$('#username').val(),
password:
$('#password').val()
},
type: 'POST',
dataType:
'json'
});
}
if (checkPassword()) {
// Tell the user
they're logged in
}
This code misunderstands the above asynchrony issues.
Specifically, $.ajax() doesn't freeze the code while it checks the '/password'
page on your server - it sends a request to the server and while it waits,
immediately returns a jQuery Ajax Deferred object, not the response from the
server. That means the if statement is going to continuously get this Deferred
object, treat it as true, and continue as nevertheless the user is logged in.
Not good.
But the fix is easy:
checkPassword()
.done(function(r) {
if (r) {
// Tell the
user they're logged in
} else {
// Tell the
user their password was bad
}
})
.fail(function(x) {
// Tell the user
something bad happened
});
Not recommended: Synchronous "Ajax" calls
As I stated, some (!) asynchronous processes have
synchronous equivalents.
I don't promote their use, but for completeness' sake, here is how you would do a synchronous call:
I don't promote their use, but for completeness' sake, here is how you would do a synchronous call:
Without jQuery
If you straightly use a XMLHTTPRequest object, pass false as
3rd argument to .open.
jQuery
If you use jQuery, you can set the value of async option to
false. Note that this option is deprecated as jQuery 1.8.
You can then also still use a success callback or access the responseText stuff of the jqXHR object:
You can then also still use a success callback or access the responseText stuff of the jqXHR object:
function foo() {
var jqXHR =
$.ajax({
//...
async: false
});
return
jqXHR.responseText;
}
If you use any additional jQuery Ajax method, such as $.get,
$.getJSON, etc., you have to alter it to $.ajax (as you can only pass configuration
parameters to $.ajax).
Comments
Post a Comment