Oh jeez, these are pretty deep questions and this blog looks pretty long... Well, I am going to try my best to help you gain a basic understanding of the event loop, how it works, and why it’s used.
Note: Prerequisites should include a general or light understanding of JavaScript, callback functions, AJAX, and web APIs.
To begin, let’s cover a topic that I think is pretty important when learning about JS. JS is a single-threaded interpretative language.
What this means is that JS has to parse(read) the code and execute the code line by line. JS cannot execute the next line of code until the previous line has finished executing. How does JS do this? JS uses something named a call stack to keep track of where you are in the code by adding the functions it read into a task queue. Then there is a process by which JS executes the functions in order of the next task in the task queue. Once the function is finished, it is cleared from the call stack and ready for the next bit of code. A good simple example of this in action is the following:
function sayMyName() {
[1,2].forEach(num => {
alert(`${num}. Say my name`);
});
console.log(“‘Say my name’ has been said 2 times, was anyone around you?”);
}sayMyName();
Go ahead and plug that into your browser’s console. In this function, you will notice that you get 2 alerts before seeing the printed console message. Hopefully, the result is as you expected (you are destined to ‘Say my name’ 2 times). Take note of this for a second did you notice something interesting?
Unless you click ‘ok’, you will not see the console message. That seems not so awesome, right? To be blocked from running other code while the alert is waiting for the user to click ‘ok’? Well, this is an example of how the single-threaded language operates using the call stack and how it can be a bit restricting with synchronous code execution.
Another example to explain the call stack is this function:
function stackerror() {
return stackerror();
}stackerror();
If you run this function, you will see the error:
Uncaught RangeError: Maximum call stack size exceeded
This error is from adding too many functions to the task queue by endlessly invoking stackerror(). The task queue gets too large, therefore chrome is stopping this function from adding anything more to the task queue and preventing memory-related issues.
OK. That seemed easy enough to understand right? Now let’s quickly dive into how this is managed.
So, we have this thing called the runtime environment. Each browser has a runtime environment that runs a JavaScript engine that reads and executes the JS code. For example, in Chrome, we have V8 which is a JavaScript engine developed by ‘The Chromium Project’ back in 2008.
V8 is the engine that is managing the functions added and removed in the task queue or web API, where and how the memory is stored and released in your browser and computer (memory heap), and essentially is the thing that manages the event loop. V8 also is what provides the browser with JS data types, objects, and functions.
Wow, that’s a big deal, if it feels like this topic can easily cause you to go down a rabbit hole of research, you are right. That is exactly what I am going to do after writing this :). I encourage you to do the same and click on any links I’ve provided.
We now have a pretty basic understanding of how JS, a single-threaded interpretive language operates. Now let’s tackle another aspect of the event loop, synchrony vs. asynchrony.
Up until this point, we have been going over how JS will run synchronously by waiting to execute the next block of code until the previous one is completed. So how does this get broken up and allow us to do other stuff and not get blocked?
Well, what if we want to write code that executes out of order or asynchronously? That’s where asynchronous callback functions come into place. An asynchronous call back function is a function that will accept a function as an argument such that once the function is complete, the callback function will be invoked.
Here’s a familiar example:
let body = document.querySelector("body")
let h1 = document.createElement("h1")
let btn = document.createElement("button")h1.innerText = "Async Example"
btn.innerText = "Click Me"
body.append(h1)
body.appendChild(btn)btn.addEventListener("click", (e) => {
alert("I've Been Clicked")let p = document.createElement('p');
p.textContent = 'Chek out this paragraph that was created after the click event!';
document.body.appendChild(p);}
Create and open up a new index.html file (no need to do anything else) in your browser. Open up the console (in chrome command + option+J ) and copypasta that code above. Test it out.
What did you see? Well, you may have noticed that when you click the button, you asynchronously called the event callback function which executed the alert indicating the button was clicked, and then once you pressed ‘ok’, the p tag and contents were written which was done synchronously after the event and alert occurred.
This exemplifies a callback function passed in as an argument that was called asynchronously as it was awaiting an event out of the order of tasks to execute.
What is happening concerning the stack? Well, high level, the interpreter reads the code, and as it reads the lines of code it puts the code in the task queue. The engine moves the event listener from the task queue to the call stack where it is waiting for a click event to occur in the browser. Once the click event occurs, the ‘onClick’ (e) callback function is moved from the task (call back) queue to the call stack and starts executing the code block. Which is moving the alert function to the call stack from the queue where it awaits a click on the ‘ok’ to move on to the next code which is moved to the call stack to write the p tag and it’s contents.
Yes, this is confusing! Yes, there are great videos and content on this! Here is an awesome visual representation of this.
We are finally here: The Event Loop! If you read this far and understand what I am writing here, you deserve a cookie. Get up, stretch, and get a cookie.
Ok, so what does the event loop do? The event loop is the thing that waits for the call stack to be clear before adding a task from the task queue to the call stack.
Wait, really that’s all it does? Yea! Let’s get another example to see how the event loop works with an async web API call.
Essentially, the event loop waits for the right moment to add the next task in the task queue to the call stack unless the task relieves control back to the event loop to either clear or move back to the task queue.
Let’s say I want to render images, after fetching some data from an API? You may be familiar with a pattern like this to render a list of images:
function fetchImages(){
fetch(someURL)
.then(response => response.json())
.then(imagesJson => {
imagesJson.forEach(img => renderImg(img))
});
}function renderImg(img){
let li = document.createElement("li")
let ul= document.querySelector("ul")
li.innerHtml = `<img src=${img.src}>`
ul.append(li)
}
In this function, you can see a newish thing here called a fetch function and a .then method which is dependant on something called a promise. Woof that’s a lot… In this case, a promise is just an object that returns the results of the asynchronous fetch operation.
It’s a conversation that may sound kinda like this: “Did the request go ok? Awesome! you have fulfilled your promise! Well .then let me do something with that result or Oh no? did it fail? you rejected your promise. .then let me know what failed and we will handle it with a call back to…”
In this example, we have a fetch request that is an asynchronous call to get some data from some URL. The event loop is going to wait until the stack is clear, and send this fetch function from the task queue to the call stack to be executed. The fetch will return a promise with a response object, if other items are in the task queue, the event loop will move the next task into the call stack and continue running code after the fetch. Eventually, we will get the fulfilled or rejected promise, the .then will be added to the queue, and the event loop will wait until the stack is clear and run the .then method.
At this point, we can run other operations that can run synchronously. For example, if the returned promise fails, maybe we want to have a callback function, that can alert the user, “Hey the thing you asked for wasn’t found! 404” or in this case, if it was successful, we then want to parse the response object (to JSON) via a response.json() function as an implicit return and then render the results of the parsed response object the function renderImg(). In this example, we can see why we want to use these asynchronous functions for things like Web API calls and optimize utilizing the event loop to properly execute functions instead of waiting around for some user to do something or for a timer to run out, or for a line of code to finish, etc.
Ok. I am going to stop here. This was definitely enough. This is new for me, so if there are corrections that need to be made here please email me or let me know! I am writing this to solidify my understanding of this complicated topic as well.
Thanks and see ya in the next post!