Flow control

JavaScript is a single-threaded asynchronous language, meaning that it doesn't really fork in the same way you would expect a multi-threaded language like C++ to, and thus a function blocking the main thread causes everything to grind to a halt. Luckily, the asynchronous part of that means functions generally won't do this, and functions can be written so that the next one can fire before the first one finishes.

On the one hand, this is one of the most interesting and powerful aspects of JavaScript as a language. On the other, it makes things somewhat more difficult to reason about, and adds a degree of complexity to organizing one's code.
While you can do pretty much anything without getting too bogged down in the JavaScript event model, the one place where the asynchronous nature of JavaScript is particularly visible is when you make a request -- no matter how fast broadband Internet gets, so long as it conforms to the standard model of physics, there will always be some degree of latency while data moves from one place to another. If JavaScript blocked the rest of your application while it started and waited for every HTTP request to finish, web pages would become sluggish to the point of being unusable. This is particularly true with modern JavaScript-based web applications, which potentially make dozens of requests in the background while you browse a site.

The traditional way of handling a request is to begin the request and then supply a function that is run once the request is complete, usually as a callback:

function done() { 
console.log(this.responseText); // Finished!
}

const req = new XMLHttpRequest();
req.addEventListener('load', done)
req.open('get', 'http://www.packt.com');
req.send();

Here, we use a callback function named done that prints the contents of the request to the console as a string. While it's pretty easy to understand what's going on in such a trivial example, it becomes much more difficult when you have to chain requests:

const req1 = new XMLHttpRequest(); 

req1.addEventListener('load', function done1() {
const response1 = JSON.parse(this.textContent);
const req2 = new XMLHttpRequest();

req2.addEventListener('load', function done2() {
console.log(this.textContent); // finished!
});

req2.get(response1.newEndpoint);
req2.send();
});

req1.get('http://www.aendrew.com/api/1.json');
req1.send();

Here, we make one request, wait for it to finish, get a value from the JSON data we received, then make another request based on that value. If we wanted to make a third request using a value from the second request, we'd have to nest everything even further. This is commonly referred to in JavaScript development circles as Callback Hell, because it's really hard to read and reason about. Unfortunately, while D3's request methods are a bit less verbose than XMLHttpRequest, they still use the old school callback syntax:

d3.json('http://www.aendrew.com/api/1.json', (error1, data1) => { 
if (data1) {
d3.json(data1.newEndpoint, (error2, data2) => {
if (data2) {
// ...and so on...
}
});
}
});

After only two requests, we're already four levels of indentation deep. We could rewrite this in a few ways to make this more readable, but it's already starting to become apparent how annoying it can be to organize code in this fashion.

Fortunately, you are in no way chained to D3's request functions in order to acquire data! In fact, there are very many ways you can get data and structure your code in modern JavaScript, some of which include the following:

  • Promises
  • Generators
  • Observables

We'll go through each of these momentarily, and how to use them with D3. Note that you can use them for far more than simply waiting for data, as you'll see when we talk about generators.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset