10. Event Loop and Asynchronous Behavior

Overview

In this chapter, you'll investigate how a web page actually works within the browser, with a special focus on how, when, and why the browser executes the JavaScript code we provide. You'll dive deep into the intricacies of the event loop and see how we can manage it. Lastly, you'll learn about the tools that TypeScript offers you. By the end of this chapter, you will be able to better manage the asynchronous nature of the execution.

Introduction

In the previous chapter, you learned the fundamentals of generics and conditional types. This chapter introduces you to event loops and asynchronous behavior. However, before you proceed with learning these topics, let's have a look at a hypothetical scenario to really understand how synchronous and asynchronous executions work.

Imagine a small bank that has a single teller. His name is Tom, and he's serving clients all day. Since it's a small bank and there are few clients, there's no queue. So, when a client comes in, they get Tom's undivided attention. The client provides all the necessary paperwork, and Tom processes it. If the process needs some kind of outside input, such as from a credit bureau or the bank's back-office department, Tom submits the request, and he and the client wait for the response together. They might chat a bit, and when the response comes, Tom resumes his work. If a document needs to be printed, Tom sends it to the printer that's right on his desk, and they wait and chat. When the printing is done, Tom resumes his work. Once the work is completed, the bank has another satisfied client, and Tom continues with his day. If somebody comes while Tom is serving a client (which happens seldom), they wait until Tom is completely done with the previous client, and only then do they begin their process. Even if Tom is waiting on an external response, the other client will have to wait their turn, while Tom idly chats with the current client.

Tom effectively works synchronously and sequentially. There are lots of benefits of this approach to working, namely, Tom (and his bosses) can always tell whether he is serving a client or not, he always knows who his current client is, and he can completely forget all the data about the client as soon as the client leaves, knowing that they have been serviced completely. There are no issues with mixing up documents from different clients. Any problems are easy to diagnose and easy to fix. And since the queue never gets crowded, this setup works to everyone's satisfaction.

So far, so good. But what happens when the bank suddenly gets more clients? As more and more clients arrive, we get a long queue, and everyone is waiting, while Tom chats with the current client, waiting on a response from the credit bureau. Tom's boss is, understandably, not happy with the situation. The current system does not scale – at all. So, he wants to change the system somehow, to be able to serve more clients. How can he do that? You will look at a couple of solutions in the following section.

The Multi-Threaded Approach

Basically, there are two different approaches. One is to have multiple Toms. So, every single teller will still work in the exact same simple and synchronous way as before – we just have lots of them. Of course, the boss will need to have some kind of organization to know which teller is available and which is working, whether there are separate queues for each teller, or a single large queue, along with some kind of distribution mechanism (that is, a system where a number is assigned to each customer). The boss might also get one of those big office printers, instead of having one printer per teller, and have some kind of rule in order to not mix up the print jobs. The organization will be complex, but the task of every single teller will be straightforward.

By now, you know we're not really discussing banks. This is the usual approach for server-side processing. Grossly simplified, the server process will have multiple sub-processes (called threads) that will work in parallel, and the main process will orchestrate everything. Each thread will execute synchronously, with a well-defined beginning, middle, and end. Since servers are usually machines with lots of resources, with heavy loads, this approach makes sense. It can accommodate low or high loads nicely, and the code that processes each request can be relatively simple and easy to debug. It even makes sense to have the thread wait for some external resource (a file from the file system, or data from the network or database), since we can always spin up new threads if we have more requests. This is not the case with real live tellers. We cannot just clone a new one if more clients come. The kind of waiting done by the threads (or by Tom) is usually referred to as busy waiting. The thread is not doing anything, but it's not available for any work, since it's busy doing something – it's busy waiting. Just like Tom was actually busy chatting with the client while waiting for a response from the credit bureau.

We have a system that can be massively parallel and concurrent, but still, each part of it is run synchronously. The benefit of this approach is that we can serve many, many clients at the same time. One obvious downside is the cost, both in hardware and in complexity. While we managed to keep the client processing simple, we'll need a huge infrastructure that takes care of everything else – adding tellers, removing tellers, queueing customers, managing access to the office printer, and similar tasks.

This will use all the available resources of the bank (or the server), but that is fine, since that's the whole point – to serve clients, as many and as fast as possible, and nothing else.

However, there is another approach – asynchronous execution.

The Asynchronous Execution Approach

The other approach, the one taken by the web and, by extension, JavaScript and TypeScript, is to use just a single thread – so Tom is still on his own. But, instead of Tom idly chatting with a waiting client, he could do something else. If a situation arises where he needs some verification from the back office, he just writes down what he was doing and how far he got on a piece of paper, gives that paper to the client, and sends them to the back of the queue. Tom is now ready to start serving the next client in line. If that client does not need external resources, they are processed completely and are free to leave. If they need something else that Tom needs to wait for, they too are sent to the back of the line. And so on, and so forth. This way, if Tom has any clients at all, he's processing their requests. He's never busy waiting, instead, he's busy working. If a client needs to wait for a response, they do so separately from Tom. The only time Tom is idle is when he has no clients at all.

The benefit of this approach is fairly obvious – before, Tom spent a lot of his time chatting, now he is working all the time (of course, this benefit is from Tom's boss' point of view – Tom liked the small talk). An additional benefit is that we know our resource consumption up front. If we only have one teller, we know the square footage that we will need for the office. However, there are some downsides as well. The most important downside is that our clients now have to know our process quite intimately. They will need to understand how to queue and requeue, how to continue working from where they left off, and so on. Tom's work also got a lot more complicated. He needs to know how to pause the processing of a client, how to continue, how to behave if an external response is not received, and so on. This model of working is usually called asynchronous and concurrent. Doing his job, Tom will jump between multiple clients at the same time. More than one client will have their process started but not finished. And there's no way for a client to estimate how long it will take to process their task once it is started – it depends on how many other clients Tom processes at the same time.

From the early days, this model made much more sense for the web. For starters, web applications are processed on the device of the client. We should not make any technical assumptions about it – as we cannot be sure about the kind of device that the client might be using. In essence, a web page is a guest on the client's device – and it should behave properly. For example, using up all of a device's resources to show what amounts to a fancy animation is not proper behavior at all. Another important issue is security. If we think of web pages as applications that contain some code, we're basically executing someone's code on our machine whenever we enter a web address in the browser's address bar.

The browser needs to make sure that the code on the page, even if it's malicious, is restricted in what it can do to our machine. The web would not have been as popular as it is today if visiting a website could make your computer explode.

So, since the browser cannot know in advance which pages it will be used for, it was decided that each web page will only get access to a single thread. Also, for security reasons, each web page will get a separate thread, so a running web page cannot meddle in the execution of other pages that may execute at the same time (with features such as web workers and Chrome applications, these restrictions are somewhat loosened, but in principle, they still apply).

There is simply no way for a web page to spawn enough threads to swarm the system, or for a web page to get the data from another web page. And, since a web page needs to do lots of things at once, using the synchronous and sequential approach was out of the question. That is why all the JavaScript execution environments completely embraced the asynchronous, concurrent approach. This was done to such an extent that some common synchronization techniques are, intentionally, just not available in JavaScript.

For example, lots of other languages have a "wait some time" primitive, or a library function that does that. For example, in the C# programming language, we can have this code:

Console.WriteLine("We will wait 10 s");

Thread.Sleep(10000);

Console.WriteLine("... 10 seconds later");

Thread.Sleep(15000);

Console.WriteLine("... 15 more seconds later");

This code will write some text to the console, and 10 seconds later, write some more text. During the 25 seconds of the wait, the thread this executes on will be completely non-responsive, but the code written is simple and linear – easily understood, easily changeable, and easily debuggable. JavaScript simply does not have such a synchronous primitive, but it has an asynchronous variant in the setTimeout function. The simplest equivalent code would be the following:

console.log("We will wait 10 s");

setTimeout(() => {

    console.log("... 10 seconds later");

    setTimeout(() => {

        console.log("... 15 more seconds later");

    }, 15000);

}, 10000);

It's obvious that this code is much more complex than the C# equivalent, but the advantage that we get is that this code is non-blocking. In the 25 total seconds that this code is executing, our web page can do everything it needs to do. It can respond to events, the images can load and display, we can resize the window, scroll the text – basically, the application will resume the normal and expected functionalities.

Note that while it's possible to block the JavaScript execution with some special synchronous code, it's not easy to do it. When it does actually happen, the browser can detect that it did happen and terminate the offending page:

Figure 10.1: Unresponsive page

Figure 10.1: Unresponsive page

Executing JavaScript

When a JavaScript execution environment, such as a node or a browser loads a JavaScript file, it parses it and then runs it. All the functions that are defined in a JavaScript file are registered, and all the code that is not in a function is executed. The order of the execution is according to the code's position in the file. So, consider a file having the following code:

console.log("First");

console.log("Second");

The console will always display this:

First

Second

The order of the output cannot be changed, without changing the code itself. This is because the line with First will be executed completely – always – and then, and only then, will the line with Second begin to execute. This approach is synchronous because the execution is synchronized by the environment. We are guaranteed that the second line will not start executing, until and unless the line above it is completely done. But what happens if the line calls some function? Let's take a look at the following piece of code:

function sayHello(name){

    console.log(`Hello ${name}`);

}

function first(){

    second();

}

function second(){

    third();

}

function third(){

    sayHello("Bob");

}

first();

When the code is parsed, the environment will detect that we have four functions – first, second, third, and sayHello. It will also execute the line of code that is not inside a function (first();), and that will start the execution of the first function. But that function, while it's executing, calls the second function. The runtime will then suspend the running of the first function, remember where it was, and begin with the execution of the second function. This function, in turn, calls the third function. The same thing happens again – the runtime starts executing the third function, remembering that once that function is done, it should resume with the execution of the second function, and that once second is done, it should resume with the execution of the first function.

The structure the runtime uses to remember which function is active, and which are waiting, is called a stack, specifically, the call stack.

Note

The term "stack" is used in the sense of a stack of dishes, or a stack of pancakes. We can only add to the top, and we can only remove from the top.

The executing functions are put one on top of the other, and the topmost function is the one being actively executed, as shown in the following representation:

Figure 10.2: Stack

Figure 10.2: Stack

In the example, the third function will call the sayHello function, which will in turn call the log function of the console object. Once the log function finishes executing, the stack will start unwinding. That means that once a certain function finishes executing, it will be removed from the stack, and the function below it will be able to resume executing. So, once the sayHello function finishes executing, the third function will resume and finish in turn. This will trigger the continuation of the second function, and when that function is done as well, the first function will continue, and eventually finish. When the first function finishes executing, the stack will become empty – and the runtime will stop executing code.

It's worth noting that all of this execution is done strictly synchronously and deterministically. We can deduce the exact order and number of function calls just from looking at the code. We can also use common debugging tools such as breakpoints and stack traces.

Exercise 10.01: Stacking Functions

In this exercise, we'll define few simple functions that call each other. Each of the functions will log to the console when it starts executing and when it's about to finish executing. We will analyze when and in what order the output is mapped to the console:

Note

The code files for this exercise can be found at https://packt.link/X7QZQ.

  1. Create a new file, stack.ts.
  2. In stack.ts, define three functions called inner, middle, and outer. None of them need to have parameters or return types:

    function inner () {

    }

    function middle () {

    }

    function outer () {

    }

  3. In the body of the inner function, add a single log statement, indented by four spaces:

    function inner () {

        console.log(" Inside inner function");

    }

  4. In the body of the middle function, add a call to the inner function. Before and after the call, add a log statement, indented by two spaces:

    function middle () {

        console.log(" Starting middle function");

        inner();

        console.log(" Finishing middle function");

    }

  5. In the body of the outer function, add a call to the middle function. Before and after the call, add a log statement:

    function outer () {

        console.log("Starting outer function");

        middle();

        console.log("Finishing outer function");

    }

  6. After the function declaration, create a call only to the outer function:

    outer();

  7. Save the file, and compile it with the following command:

    tsc stack.ts

  8. Verify that the compilation ended successfully and that there is a stack.js file generated in the same folder. Execute it in the node environment with this command:

    node stack.js

    You will see the output looks like this:

    Starting outer function

      Starting middle function

        Inside inner function

      Finishing middle function

    Finishing outer function

The output shows which function started executing first (outer), as that is the first message displayed. It can also be noted that the middle function finished executing after the inner function was already finished, but before the outer function was finished.

Browsers and JavaScript

When a web page is requested by the user, the browser needs to do lots of things. We won't go into the details of each of them, but we'll take a look at how it handles our code.

First of all, the browser sends the request to the server and receives an HTML file as a response. Within that HTML file, there are embedded links to resources that are needed for the page, such as images, stylesheets, and JavaScript code. The browser then downloads those as well and applies them to the downloaded HTML. Images are displayed, elements are styled, and JavaScript files are parsed and run.

The order in which the code is executed is according to the file's order in the HTML, then according to the code's position in the file. But when are the functions called? Let's say we have the following code in our file:

function sayHello() {

    console.log("Hello");

}

sayHello();

First, the sayHello function is registered, and then when it's called later, the function actually executes and writes Hello to the console. Take a look at the following code now:

function sayHello() {

    console.log("Hello");

}

function sayHi() {

    console.log("Hi");

}

sayHello();

sayHi();

sayHello();

When the file with the preceding code is processed, it will register that it has two functions, sayHello and sayHi. Then it will detect that it has three invocations, that is, there are three tasks that need to be processed. The environment has something that is called the task queue, where it will put all the functions that need to be executed, one by one. So, our code will be transformed into three tasks. Then, the environment will check if the stack is actually empty, and if it is, it will take the first task off the queue and start executing it. The stack will grow and shrink depending on the execution of the code of the first task, and eventually, when the first task is finished, it will be empty. So, after the first task is executed, the situation will be as follows:

  1. The execution stack will be empty.
  2. The task queue will contain two tasks.
  3. The first task will be completely done.

Once the stack is empty, the next task is dequeued and executed, and so on, until both the task queue and the stack are empty, and all the code is executed. Again, this whole process is done synchronously, in a specified order.

Events in the Browser

Now, take a look at a different example:

function sayHello() {

    console.log("Hello");

}

document.addEventListener("click", sayHello);

If you have this code in a JavaScript file that is loaded by the browser, you can see that the sayHello function is registered but not executed. However, if you click anywhere on the page, you will see that the Hello string appears on the console, meaning the sayHello function got executed. If you click multiple times, you'll get multiple instances of "Hello" on the console. And this code did not invoke the sayHello function even once; you don't have the sayHello() invocation in the code at all.

What happened is, you registered our function as an event listener. Consider that you don't call our function at all, but the browser's environment will call it for us, whenever a certain event occurs – in this case, the click event on the whole document. And since those events are generated by the user, we cannot know if and when our code will execute. Event listeners are the principal way that our code can communicate with the page that it's on, and they are called asynchronously – you don't know when or if the function will be invoked, nor how many times it will be invoked.

What the browser does, when an event occurs, is to look up its own internal table of registered event handlers. In our case, if a click event occurs anywhere on the document (that's the whole web page), the browser will see that you have registered the sayHello function to respond to it. That function will not be executed directly – instead, the browser will place an invocation of the function in the task queue. After that, the regular behavior explained previously takes effect. If the queue and stack are empty, the event handler will begin executing immediately. Otherwise, our handler will wait for its turn.

This is another core effect of asynchronous behavior – we simply cannot guarantee that the event handler will execute immediately. It might be the case that it does, but there is no way to know if the queue and stack are empty at a specific moment. If they are, we'll get immediate execution, but if they're not, we'll have to wait our turn.

Environment APIs

Most of our interaction with the browser will be done in the same pattern – you will define a function, and pass that function as a parameter to some browser API. When and if that function will actually be scheduled for execution will depend on the particulars of that API. In the previous case, you used the event handler API, addEventListener, which takes two parameters, the name of an event, and the code that will be scheduled when that event happens.

Note

You can get a list of different possible events at https://developer.mozilla.org/en-US/docs/Web/Events.

In the rest of this chapter, you will use two other APIs as well, the environment's method to defer some code for later execution (setTimeout) and the ability to call on external resources (popularly called AJAX). There are two different AJAX implementations that we will be working with, the original XMLHttpRequest implementation, and the more modern and flexible fetch implementation.

setTimeout

As mentioned previously, the environment offers no possibility to pause the execution of JavaScript for a certain amount of time. However, the need to execute some code after some set amount of time has passed arises quite often. So, instead of pausing the execution, we get to do something different that has the same outcome. We get to schedule a piece of code to get executed after an amount of time has passed. To do that we use the setTimeout function. This function takes two parameters: A function that will need to be executed, and the time, in milliseconds, it should defer the execution of that function by:

setTimeout(function() {

    console.log("After one second");

}, 1000);

Here it means that the anonymous function that is passed as a parameter will be executed after 1,000 milliseconds, that is, one second.

Exercise 10.02: Exploring setTimeout

In this exercise, you'll use the setTimeout environment API call to investigate how asynchronous execution behaves and what it does:

Note

The code files for this exercise can be found at https://packt.link/W0mlS.

  1. Create a new file, delays-1.ts.
  2. In delays-1.ts, log some text at the beginning of the file:

    console.log("Printed immediately");

  3. Add two calls to the setTimeout function:

    setTimeout(function() {

        console.log("Printed after one second");

    }, 1000);

    setTimeout(function() {

        console.log("Printed after two second");

    }, 2000);

    Here, instead of creating a function and giving it to the setTimeout function using its name, we have used an anonymous function that we have created in-place. We can also use arrow functions instead of functions defined with the function keyword.

  4. Save the file, and compile it with the following command:

    tsc delays-1.ts

  5. Verify that the compilation ended successfully and that there is a delays-1.js file generated in the same folder. Execute it in the node environment with this command:

    node delays-1.js

    You will see the output looks like this:

    Printed immediately

    Printed after one second

    Printed after two second

    The second and third lines of the output should not appear immediately, but after 1 and 2 seconds respectively.

  6. In the delays-1.ts file, switch the two calls to the setTimeout function:

    console.log("Printed immediately");

    setTimeout(function() {

        console.log("Printed after two second");

    }, 2000);

    setTimeout(function() {

        console.log("Printed after one second");

    }, 1000);

  7. Compile and run the code again, and verify that the output behaves identically. Even if the former setTimeout was executed first, its function parameter is not scheduled to run until 2 seconds have passed.
  8. In the delays-1.ts file, move the initial console.log to the bottom:

    setTimeout(function() {

        console.log("Printed after two second");

    }, 2000);

    setTimeout(function() {

        console.log("Printed after one second");

    }, 1000);

    console.log("Printed immediately");

  9. Compile and run the code again, and verify that the output behaves identically. This illustrates one of the most common problems with code that behaves asynchronously. Even though the line was at the bottom of our file, it was executed first. It's much harder to mentally trace code that does not follow the top-down paradigm we're used to.
  10. Create a new file, delays-2.ts.
  11. In delays-2.ts, add a single call to the setTimeout function, and set its delay time to 0. This will mean that our code needs to wait 0 milliseconds in order to execute:

    setTimeout(function() {

        console.log("#1 Printed immediately?");

    }, 0);

  12. Add a console.log statement after the call to setTimeout:

    console.log("#2 Printed immediately.");

  13. Save the file, and compile it with the following command:

    tsc delays-2.ts

  14. Verify that the compilation ended successfully and that there is a delays-2.js file generated in the same folder. Execute it in the node environment with this command:

    node delays-2.js

    You will see the output looks like this:

    #2 Printed immediately.

    #1 Printed immediately?;

    Well, that looks unexpected. Both lines appear basically immediately, but the one that was in the setTimeout block, and was first in the code, came after the line at the bottom of the script. And we explicitly told setTimeout not to wait, that is, to wait 0 milliseconds before the code got executed.

To understand what happened, we need to go back to the call queue. When the file was loaded, the environment detected that we had two tasks that needed to be done, the call to setTimeout and the bottom call to console.log (#2). So, those two tasks were put into the task queue. Since the stack was empty at that time, the setTimeout call started executing, and #2 was left in the task queue. The environment saw that it has a zero delay, so immediately took the function (#1), and put it at the end of the task queue, after #2. So, after the setTimeout call was done, we were left with two console.log tasks in the queue, with #2 being the first, and #1 being the second.

They got executed sequentially, and on our console, we got #2 first, and #1 second.

AJAX (Asynchronous JavaScript and XML)

In the early days of the web, it was not possible to get data from a server once the page was loaded. That was a huge inconvenience for developing dynamic web pages, and it was solved by the introduction of an object called XMLHttpRequest. This object enabled developers to get data from a server after the initial page load – and since loading data from a server means using an external resource, it had to be done in an asynchronous manner (even if it has XML right in the name, currently, it will mostly be used for JSON data). To use this object, you'll need to instantiate it and use a few of its properties.

To illustrate its usage, we'll try to get data about William Shakespeare from the Open Library project. The URL that we'll use to retrieve that information is https://openlibrary.org/authors/OL9388A.json, and the access method that we will use is GET, as we will only be getting data.

The data received is of a specific format, defined by Open Library, so you'll start by creating an interface for the data that you will actually use. You'll display only an image of the Bard (received as an array of photo IDs), and the name, so you can define the interface like this:

interface OpenLibraryAuthor {

  personal_name: string;

  photos: number[];

}

Next, create the XMLHttpRequest object, and assign it to a variable called xhr:

const xhr = new XMLHttpRequest();

Now you need to open a connection to our URL:

const url = "https://openlibrary.org/authors/OL9388A.json";

xhr.open("GET", url);

This call doesn't actually send anything, but it prepares the system for accessing the external resource. Lastly, you need to actually send the request, using the send method:

xhr.send();

Since the request is asynchronous, this call will execute and finish immediately. In order to actually process the data once this request is done, you need to add something to this object – a callback. That is a function that will not be executed by us, but by the xhr object, once some event happens. This object has several events, such as onreadystatechange, onload, onerror, ontimeout, and you can set different functions to react to different events, but in this case, you will just use the onload event. Create a function that will get the data from the response and show it on the web page where our script is running:

const showData = () => {

  if (xhr.status != 200) {

    console.log(`An error occured ${xhr.status}: ${xhr.statusText}`);

  } else {

    const response: OpenLibraryAuthor = JSON.parse(xhr.response);

    const body = document.getElementsByTagName("body")[0];

    

    const image = document.createElement("img");

    image.src = `http://covers.openlibrary.org/a/id/${response.photos[0]}-M.jpg`;

    body.appendChild(image);

    const name = document.createElement("h1");

    name.innerHTML = response.personal_name;

    body.appendChild(name);

  }

};

In this method, you will be using some properties of the xhr variable that was defined previously, such as status, which gives us the HTTP status code of the request, or response, which gives us the actual response. If we just call the showData method by ourselves, we'll most likely get empty fields or an error, as the response will not have finished. So, we need to give this function to the xhr object, and it will use it to call the showData back:

xhr.onload = showData;

Save this code as shakespeare.ts, compile it, and add it to an HTML page using the following:

    <script src="shakespeare.js"></script>

You will get a result similar to the following:

Figure 10.3: Retrieved image of William Shakespeare

Figure 10.3: Retrieved image of William Shakespeare

Activity 10.01: Movie Browser Using XHR and Callbacks

As a TypeScript developer, you have been tasked with creating a simple page to view movie data. The web page will be simple, with a text input field and a button. When you enter the name of a movie in the search input field and press the button, general information about the movie will be displayed on the web page, along with some images relevant to the movie.

You can use The Movie Database (https://www.themoviedb.org/) as a source of general data, specifically its API. You need to issue AJAX requests using XmlHttpRequest, and use the data the site provides to format your own object. When using an API the data will rarely, if ever, be in the format we actually need. This means that you will need to use several API requests to get our data, and piecemeal construct our object. A common TypeScript approach to this issue is to use two sets of interfaces – one that exactly matches the format of the API, and one that matches the data that you use in your application. In this activity, you need to use the Api suffix to denote those interfaces that match the API format.

Another important thing to note is that this particular API does not allow completely open access. You'll need to register for an API key and then send it in each API request. In the setup code for this activity, three functions (getSearchUrl, getMovieUrl, getPeopleUrl) will be provided that will generate the correct URLs for the needed API requests, once the apiKey variable is set to the value you will receive from The Movie Database. Also provided will be the base HTML, styling, as well as the code used to actually display the data – all that is missing is the data itself.

Those resources are listed here:

  • display.ts – A TypeScript file that houses the showResult and clearResults methods, which you will call to display a movie and clear the screen, respectively.
  • interfaces.ts – A TypeScript file that contains the interfaces that you will use. All interfaces that have an Api suffix are objects that you will receive from The Movie Database API, and the rest (Movie and Character) will be used to display the data.
  • script.ts – A TypeScript file that has some boilerplate code that will start the application. The search function is here, and that function will be the main focus of this activity.
  • index.html – An HTML file that has the basic markup for our web page.
  • styles.css – A style sheet file that is used to style the results.

The following steps should help you with the solution:

Note

The code files for this activity can be found at https://packt.link/Qo4dB.

  1. In the script.ts file, locate the search function and verify that it takes a single string parameter and that its body is empty.
  2. Construct a new XMLHttpRequest object.
  3. Construct a new string for the search result URL using the getSearchUrl method.
  4. Call the open and send methods of the xhr object.
  5. Add an event handler for the xhr object's onload event. Take the response and parse it as a JSON object. Store the result in a variable of the SearchResultApi interface. This data will have the results of our search in a results field. If you get no results, this means that our search failed.
  6. If the search returned no results, call the clearResults method.
  7. If the search returned some results, just take the first one and store it in a variable, ignoring the other ones.
  8. Inside the onload handler, in the successful search branch, create a new XMLHttpRequest object.
  9. Construct a new string for the search result URL using the getMovieUrl method.
  10. Call the open and send method of the constructed xhr object.
  11. Add an event handler for the xhr objects's onload event. Take the response, and parse it as a JSON object. Store the result in a variable of the MovieResultApi interface. This response will have the general data for our movie, specifically, everything except the people who were involved in the movie. You will need to have another call to the API to get the data about the people.
  12. Inside the onload handler, in the successful search branch, create a new XMLHttpRequest object.
  13. Construct a new string for the search result URL using the getPeopleUrl method.
  14. Call the open and send method of the constructed xhr object.
  15. Add an event handler for the xhr object's onload event. Take the response, and parse it as a JSON object. Store the result in a variable of the PeopleResultApi interface. This response will have data about the people who were involved in the movie.
  16. Now you actually have all the data you need, so you can create your own object inside the people onload handler, which is inside the movie onload handler, which is inside the search onload handler.
  17. The people data has cast and crew properties. You'll only take the first six cast members, so first sort the cast property according to the order property of the cast members. Then slice off the first six cast members into a new array.
  18. Transform the cast data (which is CastResultApi objects) into our own Character objects. You need to map the character field of CastResultApi to the name field of Character, the name field to the actor name, and the profile_path field to the image property.
  19. From the crew property of the people data, you'll only need the director and the writer. Since there can be multiple directors and writers, you'll get the names of all directors and writers and concatenate them, respectively. For the directors, from the crew property, filter the people who have a department of Directing and a job of Director. For those objects, take the name property, and join it together with an & in between.
  20. For the writers, from the crew property, filter the people who have a department of Writing and a job of Writer. For those objects, take the name property, and join it together with an & in between.
  21. Create a new Movie object (using object literal syntax). Fill in all the properties of the Movie object using the data from the movie and people responses you prepared so far.
  22. Call the showResults function with the movie you constructed.
  23. In your parent directory (Activity01 in this case), install dependencies with npm i.
  24. Compile the program using tsc ./script.ts ./interfaces.ts ./display.ts.
  25. Verify that the compilation ended successfully.
  26. Open index.html using the browser of your choice.

    You should see the following in your browser:

    Figure 10.4: The final web page

Figure 10.4: The final web page

Note

The solution to this activity can be found via this link.

We will improve this application further in Activity 10.02, Movie Browser using fetch and Promises, and Activity 10.03, Movie Browser using fetch and async/await. However, before we do that, you need to learn about Promises and async/await in TypeScript.

Promises

Using callbacks for asynchronous development gets the job done – and that is great. However, in many applications, our code needs to use external or asynchronous resources all the time. So, very quickly, we'll get to a situation where inside our callback, there is another asynchronous call, which requires a callback inside the callback, which in turn needs a callback on its own….

It was (and in some cases, it still is) not uncommon to be a dozen levels deep inside the callback hole.

Exercise 10.03: Counting to Five

In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last word was displayed:

Note

The code files for this exercise can be found at https://packt.link/zD7TT.

  1. Create a new file, counting-1.ts.
  2. In counting-1.ts, add an array with the English number names up to and including five:

    const numbers = ["One", "Two", "Three", "Four", "Five"];

  3. Add a single call to the setTimeout function, and print out the first number after a second:

    setTimeout(function() {

        console.log(numbers[0]);

    }, 1000);

  4. Save the file, and compile it with the following command:

    tsc counting-1.ts

  5. Verify that the compilation ended successfully and that there is a counting-1.js file generated in the same folder. Execute it in the node environment with this command:

    node counting-1.js

    You will see the output looks like this:

    One

    The line should appear 1 second after the application was run.

  6. In the counting-1.ts file, inside the setTimeout function, below console.log, add another, nested, call to the setTimeout function:

    setTimeout(function() {

        console.log(numbers[0]);

        setTimeout(function() {

            console.log(numbers[1]);

        }, 1000);

    }, 1000);

  7. Compile and run the code again, and verify that the output has an extra line, displayed 1 second after the first:

    One

    Two

  8. In the counting-1.ts file, inside the nested setTimeout function, above console.log, add another nested call to the setTimeout function:

    setTimeout(function() {

        console.log(numbers[0]);

        setTimeout(function() {

            setTimeout(function() {

                console.log(numbers[2]);

            }, 1000);

            console.log(numbers[1]);

        }, 1000);

    }, 1000);

  9. In the innermost setTimeout function, below console.log, add yet another nested call to setTimeout, and repeat the process for the fifth number as well. The code should look like this:

    setTimeout(function() {

        console.log(numbers[0]);

        setTimeout(function() {

            setTimeout(function() {

                console.log(numbers[2]);

                setTimeout(function() {

                    console.log(numbers[3]);

                    setTimeout(function() {

                        console.log(numbers[4]);

                    }, 1000);

                }, 1000);

            }, 1000);

            console.log(numbers[1]);

        }, 1000);

    }, 1000);

  10. Compile and run the code again, and verify that the output appears in the correct order as shown:

    One

    Two

    Three

    Four

    Five

In this simple example, we implemented a simple functionality – counting to five. But as you can already see, the code is becoming extremely messy. Just imagine if we needed to count to 20 instead of 5. That would be a downright unmaintainable mess. While there are ways to make this specific code look a bit better and more maintainable, in general, that's not the case. The use of callbacks is intrinsically connected with messy and hard-to-read code. And messy and hard-to-read code is the best place for bugs to hide, so callbacks do have a reputation of being the cause of difficult-to-diagnose bugs.

An additional problem with callbacks is that there cannot be a unified API across different objects. For example, we needed to explicitly know that in order to receive data using the xhr object, we need to call the send method and add a callback for the onload event. And we needed to know that in order to check whether the request was successful or not, we have to check the status property of the xhr object.

What are Promises?

Fortunately, we can promise you that there is a better way. That way was initially done by third-party libraries, but it has proven to be so useful and so widely adopted that it was included in the JavaScript language itself. The logic behind this solution is rather simple. Each asynchronous call is basically a promise that, sometime in the future, some task will be done and some result will be acquired. As with promises in real life, we can have three different states for a promise:

  • A promise might not be resolved yet. This means that we need to wait some more time before we get a result. In TypeScript, we call these promises "pending."
  • A promise might be resolved negatively – the one who promised broke the promise. In TypeScript, we call these promises "rejected" and usually we get some kind of an error as a result.
  • A promise might be resolved positively – the one who promised fulfilled the promise. In TypeScript, we call these promises "resolved" and we get a value out of them – the actual result.

And since promises are objects themselves, this means that promises can be assigned to variables, returned from functions, passed as arguments into functions, and lots of other things we're able to do with regular objects.

Another great feature of promises is that it is relatively easy to write a promisified wrapper around an existing callback-based function. Let's try to promisify the Shakespeare example. We'll start by taking a look at the showData function. This function needs to do a lot of things, and those things are sometimes not connected to one another. It needs to both process the xhr variable to extract the data, and it needs to know what to do with that data. So, if the API we're using changes, we'll need to change our function. If the structure of our web page changes, that is, if we need to display a div instead of an h1 element, we'll need to change our function. If we need to use the author data for something else, we'll also need to change our function. Basically, if anything needs to happen to the response, it needs to happen then and there. We have no way to somehow defer that decision to another piece of code. This creates unnecessary coupling inside our code, which makes it harder to maintain.

Let's change that. We can create a new function that will return a promise, which will provide the data about the author. It will have no idea what that data will be used for:

const getShakespeareData = () => {

  const result = new Promise<OpenLibraryAuthor>((resolve, reject) => {

      const xhr = new XMLHttpRequest();

      const url = "https://openlibrary.org/authors/OL9388A.json";

      xhr.open("GET", url);

      xhr.send();

      xhr.onload = () => {

        if (xhr.status != 200) {

            reject({

                error: xhr.status,

                message: xhr.statusText

            })

        } else {

            const response: OpenLibraryAuthor = JSON.parse(xhr.response);

            resolve(response);

        }

      }

  });

  return result;

};

This function returns a Promise object, which was created using the Promise constructor. This constructor takes a single argument, which is a function. That function takes two arguments as well (also functions), which are by convention called resolve and reject. You can see that the function inside the constructor just creates an xhr object, calls its open and send methods, sets its onload property, and returns. So, basically, nothing gets done, except that the request is fired off.

A promise thus created will be in the pending state. And the promise stays in this state until one of the resolve or reject functions is called inside the body. If the reject function is called, it will transition to a rejected state, and we'll be able to use the catch method of the Promise object to handle the error, and if the resolve function is called, it will transition to the resolved state, and we'll be able to use the then method of the Promise object.

One thing that we should note is that this method does nothing that is UI-related. It does not print any errors on the console or change any DOM elements. It simply promises us that it will get us an OpenLibraryAuthor object. Now, we're free to use this object however we want:

getShakespeareData()

  .then(author => {

    const body = document.getElementsByTagName("body")[0];

    

    const image = document.createElement("img");

    image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;

    body.appendChild(image);

    const name = document.createElement("h1");

    name.innerHTML = author.personal_name;

    body.appendChild(name);

  })

  .catch(error => {

    console.log(`An error occured ${error.error}: ${error.message}`);

  })

In this piece of code, we call the getShakespeareData data function, and then on its result, we're calling two methods, then and catch. The then method only executes if the promise is in the resolved state and it takes in a function that will get the result. The catch method only executes if the promise is in the rejected state, and it will get the error as an argument to its function.

One important note for the then and catch methods – they also return promises. This means that Promise objects are chainable, so instead of going in depth, as we did with callbacks, we can go in length, so to say. To illustrate that point, let's count to five once again.

Note

A more comprehensive discussion of Promises will be presented in Chapter 12, Guide to Promises in TypeScript.

Exercise 10.04: Counting to Five with Promises

In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last one was displayed:

Note

The code files for this exercise can be found at https://packt.link/nlge8.

  1. Create a new file, counting-2.ts.
  2. In counting-2.ts, add an array with the English number names up to and including five:

    const numbers = ["One", "Two", "Three", "Four", "Five"];

  3. Add a promisified wrapper of the setTimeout function. This wrapper will only execute when the given timeout expires:

    const delay = (ms: number) => {

        const result = new Promise<void>((resolve, reject) => {

            setTimeout(() => {

                resolve();

            }, ms)

        });

        return result;

    }

    Since our promise will not return any meaningful result, instead just resolving after a given amount of milliseconds, we have provided void as its type.

  4. Call the delay method with a parameter of 1000, and after its resolution, print out the first number:

    delay(1000)

    .then(() => console.log(numbers[0]))

  5. Save the file, and compile it with the following command:

    tsc counting-2.ts

  6. Verify that the compilation ended successfully and that there is a counting-2.js file generated in the same folder. Execute it in the node environment with this command:

    node counting-2.js

    You will see the output looks like this:

    One

    The line should appear 1 second after the application was run.

  7. In the counting-2.ts file, after the then line, add another then line. Inside it, call the delay method again, with a timeout of 1 second:

    delay(1000)

    .then(() => console.log(numbers[0]))

    .then(() => delay(1000))

    We can do this because the result of the then method is Promise, which has its own then method.

  8. After the last then line, add another then line, inside which you print out the second number:

    delay(1000)

    .then(() => console.log(numbers[0]))

    .then(() => delay(1000))

    .then(() => console.log(numbers[1]))

  9. Compile and run the code again, and verify that the output has an extra line, displayed 1 second after the first.
  10. In the counting-2.ts file, add two more then lines for the third, fourth, and fifth numbers as well. The code should look like this:

    delay(1000)

    .then(() => console.log(numbers[0]))

    .then(() => delay(1000))

    .then(() => console.log(numbers[1]))

    .then(() => delay(1000))

    .then(() => console.log(numbers[2]))

    .then(() => delay(1000))

    .then(() => console.log(numbers[3]))

    .then(() => delay(1000))

    .then(() => console.log(numbers[4]))

  11. Compile and run the code again, and verify that the output appears in the correct order.

    Let's compare this code with the code of the previous exercise. It's not the cleanest code, but its function is relatively obvious. We can see how we could expand this code to count to 20. And the major benefit here is that this code, while asynchronous, is still sequential. We can reason about it, and the lines that are at the top will execute before the lines at the bottom. Furthermore, since we have objects now, we can even refactor this code into an even simpler and more extensible format – we can use a for loop.

  12. In the counting-2.ts file, remove the lines starting with delay(1000) until the end of the file. Add a line that will define a resolved promise:

    let promise = Promise.resolve();

  13. Add a for loop that, for each number of the numbers array, will add to the promise chain a delay of 1 second, and print the number:

    for (const number of numbers) {

        promise = promise

            .then(() => delay(1000))

            .then(() => console.log(number))

    };}

  14. Compile and run the code again, and verify that the output appears in the correct order as shown:

    One

    Two

    Three

    Four

    Five

Activity 10.02: Movie Browser Using fetch and Promises

In this activity, we will be repeating the previous activity. The major difference is that, instead of using XMLHttpRequest and its onload method, we'll be using the fetch web API. In contrast to the XMLHttpRequest class, the fetch web API returns a Promise object, so instead of nesting our callbacks to have multiple API calls, we can have a chain of promise resolutions that will be far easier to understand.

The activity has the same files and resources as the previous activity.

The following steps should help you with the solution:

  1. In the script.ts file, locate the search function and verify that it takes a single string parameter and that its body is empty.
  2. Above the search function, create a helper function called getJsonData. This function will use the fetch API to get data from an endpoint and format it as JSON. It should take a single string called url as a parameter, and it should return a Promise.
  3. In the body of the getJsonData function, add code that calls the fetch function with the url parameter, and then call the json method on the returned response.
  4. In the search method, construct a new string for the search result URL using the getSearchUrl method.
  5. Call the getJsonData function with the searchUrl as a parameter.
  6. Add a then handler to the promise returned from getJsonData. The handler takes a single parameter of the type SearchResultApi.
  7. In the body of the handler, check whether we have any results and if we don't, throw an error. If we do have results, return the first item. Note that the handler returns an object with id and title properties, but the then method actually returns a Promise of that data. This means that after the handler, we can chain other then calls.
  8. Add another then call to the previous handler. This handler will take a movieResult parameter that contains the id and title of the movie. Use the id property to call the getMovieUrl and getPeopleUrl methods to, respectively, get the correct URLs for the movie details and for the cast and crew.
  9. After getting the URLs, call the getJsonData function with both, and assign the resulting values to variables. Note that the getJsonData(movieUrl) call will return a Promise of MovieResultApi, and getJsonData(peopleUrl) will return a Promise of PeopleResultApi. Assign those result values to variables called dataPromise and peoplePromise.
  10. Call the static Promise.all method with dataPromise and peoplePromise as parameters. This will create another promise based on those two values, and this promise will be resolved successfully if and only if both (that is, all) promises that are contained within resolve successfully. Its return value will be a Promise of an array of results.
  11. Return the promise generated by the Promise.all call from the handler.
  12. Add another then handler to the chain. This handler will take the array returned from Promise.all as a single parameter.
  13. Deconstruct the parameter into two variables. The first element of the array should be the movieData variable of type MovieResultApi, and the second element of the array should be the peopleData variable of type PeopleResultApi.
  14. The people data has cast and crew properties. We'll only take the first six cast members, so first sort the cast property according to the order property of the cast members. Then slice off the first six cast members into a new array.
  15. Transform the cast data (which is CastResultApi objects) into your own Character objects. We need to map the character field of CastResultApi to the name field of Character, the name field to the actor name, and the profile_path field to the image property.
  16. From the crew property of the people data, we'll only need the director and the writer. Since there can be multiple directors and writers, we'll get the names of all directors and writers and concatenate them, respectively. For the directors, from the crew property, filter the people who have a department of Directing and a job of Director. For those objects, take the name property, and join it together with an & in between.
  17. For the writers, from the crew property, filter the people who have a department of Writing and a job of Writer. For those objects, take the name property, and join it together with an & in between.
  18. Create a new Movie object (using object literal syntax). Fill in all the properties of the Movie object using the data from the movie and people responses we've prepared so far.
  19. Return the Movie object from the handler.
  20. Note that we did not do any UI interactions in our code. We just received a string, did some promise calls, and returned a value. The UI work can now be done in UI-oriented code. In this case, that's in the click event handler of the search button. We should simply add a then handler to the search call that will call the showResults method, and a catch handler that will call the clearResults method.

Although we used fetch and promises in this activity, and our code is now much more efficient but complex, the basic function of the website will be the same and you should see an output similar to the previous activity.

Note

The code files for this activity can be found at https://packt.link/IeDTF. The solution to this activity can be found via this link.

async/await

Promises solved the problem of callbacks quite nicely. However, often, they carry with them lots of unneeded fluff. We need to write lots of then calls, and we need to be careful not to forget to close any parentheses.

The next step is to add a piece of syntactic sugar to our TypeScript skills. Unlike the other things in this chapter, this feature originated in TypeScript, and was later adopted in JavaScript as well. I'm talking about the async/await keywords. These are two separate keywords, but they are always used together, so the whole feature became known as async/await.

What we do is we can add the async modifier to a certain function, and then, in that function, we can use the await modifier to execute promises easily. Let's go once more to our Shakespearean example, and let's wrap the code we used to call getShakespeareData inside another function, simply called run:

function run() {

    getShakespeareData()

    .then(author => {

        const body = document.getElementsByTagName("body")[0];

        

        const image = document.createElement("img");

        image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;

        body.appendChild(image);

        const name = document.createElement("h1");

        name.innerHTML = author.personal_name;

        body.appendChild(name);

    })

    .catch(error => {

        console.log(`An error occured ${error.error}: ${error.message}`);

    })

}

run();

This code is functionally equivalent to the code we had previously. But now, we have a function that we can mark as an async function, like this:

async function run() {

Now, we're allowed to just get the result of a promise and put it inside of a variable. So, the whole then invocation will become this:

    const author = await getShakespeareData();

    const body = document.getElementsByTagName("body")[0];

    

    const image = document.createElement("img");

    image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;

    body.appendChild(image);

    

    const name = document.createElement("h1");

    name.innerHTML = author.personal_name;

    body.appendChild(name);

You can see that we don't have any wrapping function calls anymore. The catch invocation can be replaced with a simple try/catch construct, and the final version of the run function will look like this:

async function run () {

  try {

    const author = await getShakespeareData();

    const body = document.getElementsByTagName("body")[0];

    

    const image = document.createElement("img");

    image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;

    body.appendChild(image);

    

    const name = document.createElement("h1");

    name.innerHTML = author.personal_name;

    body.appendChild(name);

  } catch (error) {

    console.log(`An error occured ${error.error}: ${error.message}`);

  }

}

You will notice that the amount of code that is deeply nested is drastically reduced. Now we can look at the code, and have a good idea of what it does, just from a quick glance. This is still the same, deeply asynchronous code, the only difference is that it looks almost synchronous and definitely sequential.

Exercise 10.05: Counting to Five with async and await

In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last one was displayed:

Note

The code files for this exercise can be found at https://packt.link/TaH6b.

  1. Create a new file, counting-3.ts.
  2. In counting-3.ts, add an array with the English number names up to and including five:

    const numbers = ["One", "Two", "Three", "Four", "Five"];

  3. Add a promisified wrapper of the setTimeout function. This wrapper will only execute when the given timeout expires:

    const delay = (ms: number) => {

        const result = new Promise<void>((resolve, reject) => {

            setTimeout(() => {

                resolve();

            }, ms)

        });

        return result;

    }

    Since our promise will not return any meaningful results, instead of just resolving after a given number of milliseconds, we have provided void as its type.

  4. Create an empty async function called countNumbers and execute it on the last line of the file:

    async function countNumbers() {

    }

    countNumbers();

  5. Inside the countNumbers function, await the delay method with a parameter of 1000, and after its resolution, print out the first number:

    async function countNumbers() {

        await delay(1000);

        console.log(numbers[0]);

    }

  6. Save the file, and compile it with the following command:

    tsc counting-3.ts

  7. Verify that the compilation ended successfully and that there is a counting-3.js file generated in the same folder. Execute it in the node environment with this command:

    node counting-3.js

    You will see the output looks like this:

    One

    The line should appear 1 second after the application was run.

  8. In the counting-3.ts file, after the console.log line, add two more lines for the rest of the numbers as well. The code should look like this:

    async function countNumbers() {

        await delay(1000);

        console.log(numbers[0]);

        await delay(1000);

        console.log(numbers[1]);

        await delay(1000);

        console.log(numbers[2]);

        await delay(1000);

        console.log(numbers[3]);

        await delay(1000);

        console.log(numbers[4]);

    }

  9. Compile and run the code again, and verify that the output appears in the correct order.

    Since the code is completely identical for all the numbers, it's trivial to replace it with a for loop.

  10. In the counting-3.ts file, remove the body of the countNumbers function, and replace it with a for loop that, for each number of the numbers array, will await a delay of a second, and then print the number:

    for (const number of numbers) {

        await delay(1000);

        console.log(number);

    };

  11. Compile and run the code again, and verify that the output appears in the correct order:

    One

    Two

    Three

    Four

    Five

Activity 10.03: Movie Browser Using fetch and async/await

In this activity, we will be improving on the previous activity. The major difference is that instead of using the then method of the Promises class, we'll use the await keyword to do that for us magically. Instead of a chain of then calls, we'll just have code that looks completely regular, with some await statements peppered throughout.

The activity has the same files and resources as the previous activity.

The following steps should help you with the solution:

  1. In the script.ts file, locate the search function and verify that it takes a single string parameter and that its body is empty. Note that this function is now marked with the async keywords, which allows us to use the await operator.
  2. Above the search function, create a helper function called getJsonData. This function will use the fetch API to get data from an endpoint and format it as JSON. It should take a single string called url as a parameter, and it should return a promise.
  3. In the body of the getJsonData function, add code that calls the fetch function with the url parameter, and then call the json method on the returned response.
  4. In the search method, construct a new string for the search result URL using the getSearchUrl method.
  5. Call the getJsonData function with searchUrl as a parameter, and await the result. Place the result into the SearchResultApi variable.
  6. Check whether we have any results and if we don't, throw an error. If we do have results, set the first item of the result property into a variable called movieResult. This object will contain the id and title properties of the movie.
  7. Use the id property to call the getMovieUrl and getPeopleUrl methods to, respectively, get the correct URLs for the movie details and for the cast and crew.
  8. After getting the URLs, call the getJsonData function with both, and assign the resulting values to variables. Note that the getJsonData(movieUrl) call will return a promise of MovieResultApi, and getJsonData(peopleUrl) will return a promise of PeopleResultApi. Assign those result values to variables called dataPromise and peoplePromise.
  9. Call the static Promise.all method with dataPromise and peoplePromise as parameters. This will create another promise based on those two values, and this promise will be resolved successfully if and only if both (that is, all) promises that are contained within resolve successfully. Its return value will be a promise of an array of results. await this promise, and place its result into a variable of type array.
  10. Deconstruct that array into two variables. The first element of the array should be the movieData variable of type MovieResultApi, and the second element of the array should be the peopleData variable of type PeopleResultApi.
  11. The people data has cast and crew properties. We'll only take the first six cast members, so first sort the cast property according to the order property of the cast members. Then slice off the first six cast members into a new array.
  12. Transform the cast data (which is CastResultApi objects) into our own Character objects. We need to map the character field of CastResultApi to the name field of Character, the name field to the actor name, and the profile_path field to the image property.
  13. From the crew property of the people data, we'll only need the director and the writer. Since there can be multiple directors and writers, we'll get the names of all directors and writers and concatenate them, respectively. For the directors, from the crew property, filter the people who have a department of Directing and a job of Director. For those objects, take the name property, and join it together with an & in between.
  14. For the writers, from the crew property, filter the people who have a department of Writing and a job of Writer. For those objects, take the name property, and join it together with an & in between.
  15. Create a new Movie object (using object literal syntax). Fill in all the properties of the Movie object using the data from the movie and people responses we've prepared so far.
  16. Return the Movie object from the function.
  17. Note that we did not do any UI interactions in our code. We just received a string, did some promise calls, and returned a value. The UI work can now be done in UI-oriented code. In this case, that's in the click event handler of the search button. We should simply add a then handler to the search call that will call the showResults method, and a catch handler that will call the clearResults method.

    Although we used fetch and async/await in this activity, and our code is now just as efficient but less complex compared with the previous activity, the basic function of the website will be the same and you should see an output similar to the previous activity.

    Note

    The code files for this activity can be found at https://packt.link/fExtR. The solution to this activity can be found via this link.

Summary

In this chapter, we looked at the execution model that is used on the web, and how we can use it to actually execute code. We glanced at the surface of the intricacies of asynchronous development – and how we can use it to load data from external resources. We showed the problems that arise when we get too deep into the hole of callbacks and managed to exit it using promises. Finally, we were able to await our asynchronous code, and have the best of both words – code that looks like it's synchronous, but that executes asynchronously.

We also tested the skills developed in the chapter by creating a movie data viewer browser, first using XHR and callbacks, and then improved it progressively using fetch and promises, and then using fetch and async/await.

The next chapter will teach you about higher-order functions and callbacks.

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

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