Let’s start from the beginning…
When you visit a website you do so within a web browser, like Chrome, Firefox, Edge, or Safari. Each browser has a JS Runtime Environment. In the environment are Web API’s that a developer can access to build a program.
The V8 JS Engine
Think of the JS runtime environment as a big container. Within the big container are other smaller containers. As the JS engine parses the code it starts putting pieces of it into different containers.
The first container in the environment, which is also part of the V8 JS Engine, is called the ‘memory heap.’ As the V8 JS Engine comes across variables and function declarations in the code it stores them in the Heap.
The second container in the environment is called the ‘call stack.’ It is also part of the V8 JS Engine. As the JS Engine comes across an actionable item, like a function call, it adds it to the Stack.
Once a function is added to the Stack the JS engine jumps right in and starts parsing its code, adding variables to the Heap, adding new function calls to the top of the stack, or sending itself to the third container where Web API calls go.
Note - the Stack is a data structure that runs LIFO — last in first out. No function other than the one at the top of the stack will ever be in focus, and the engine will not move to the next function unless the one above it is popped off.
The Web API Container
The Web API calls that were sent to the Web API container from the Stack, like event listeners, HTTP/AJAX requests, or timing functions, sit there until an action is triggered. Either a ‘click’ happens, or the HTTP request finishes getting its data from its source, or a timer reaches its set time. In each instance, once an action is triggered, a ‘callback function’ is sent to the fourth and final container, the ‘callback queue.’
The Callback Queue
The Callback Queue will store all the callback functions in the order in which they were added. It will ‘wait’ until the Stack is completely empty. When the Stack is empty it will send the callback function at the beginning of the queue to the Stack. When the Stack is clear again, it will send over its next callback function.
Note - the Queue is a data structure that runs FIFO — first in first out. Whereas the Stack uses a push and pop (add to end take from end), the Queue uses push and shift (add to end take from beginning).
The Event Loop
Blocking vs Non-Blocking I/O
When we talk about blocking I/O, think of an infinite loop, where a function just keeps running. If the function never stops running then it will never get popped off the stack, thus ‘blocking’ the next function in the stack from ever running. Another possibility is running a function that has so much complex logic and calculations, it thus takes so much time to run that it ‘blocks’ the next function from running. These are things to be aware of when creating code, but these are more programming errors and poorly written code that block i/o, rather than it being the fault of the language.
A Typical Example
Many instructional videos and articles use an example similar to this to explain how the JS runtime environment works…
console.log('Hey, why am I last?');
If you copy and paste this code into your console you will see it prints ‘Hello’, then ‘Goodbye’, then undefined, then ‘Hey, why am I last?’. Even though the setTimeout function is called first and is supposed to run in zero seconds, it outputs last. Go through each line and try to understand the process of the JS Engine as it parses this code. Try and think why the setTimeout() call prints to the console after sayHi() and sayBye().
When you’re done thinking it through, let’s take a look at the exact way in which the V8 JS Engine handles this code…
1. The JS Engine parses the entire script checking for syntax errors. It sees none so it starts more fully parsing from the top.
2. It sees a setTimeout function call and pushes it to the top of the Stack.
3. It jumps right into the function call, sees it is part of the Web API and thus sends it over to the Web API container and pops it off the Stack.
4. Because the timer is set to 0 seconds, the Web API container immediately pushes its anonymous function to the callback queue. The event loop checks the Stack to see if it’s empty, but it is not, because…
5–6. …as soon as the setTimeout function was sent over to the Web API container, the V8 JS Engine saw two function declarations, stored them in the Heap and then saw a function call of sayHi() and pushed it to the Stack.
7. That sayHi() function calls console.log(), which is pushed to the top of the stack.
8. The JS Engine jumps right into parsing that function. It returns a message to the console - ‘Hi’ - and is popped off.
9. The JS Engine moves back into the sayHi() function, gets to its closing bracket and pops it off the Stack.
10–12. As soon it’s popped off the Stack the next function call, sayBye(), is pushed to the Stack. It is parsed, calls console.log(), which gets pushed to the top of the Stack, it returns a message, is popped off the stack, then its original function is popped off the Stack.
13. The Event Loop sees that the Stack is finally empty. It lets the Queue know and the Queue pushes its anonymous function to the Stack.
14. The anonymous function is parsed and it calls console.log(), which is pushed on the stack.
15. The console.log() function is executed and is popped off the Stack.
16. The anonymous function is popped off the Stack and the program is finally finished.
PS - If you copy and pasted the code to the console you may have noticed an ‘undefined’ result. This is because none of the main functions are returning a value. Instead, they are calling console.log(), which is being executed by the parser and getting popped off, then the parser is getting to the end of the main function without seeing a returned result, so it returns ‘undefined’, then it pops the function off the stack.
Now You Try!
To visually see the above code in action, you should use one of the greatest tools ever created for understanding the JS engine and the event loop. You can find that here. After you view the above example, reload the page and look at the provided default example. Watch the video presentation by Philip Roberts. Then try to explain this process to someone, anyone, out loud. Rubber ducky it if you have to. Then go back to the loupe tool and try writing your own examples. Predict what will happen, then run it to see if you were right. Repeat until you got this all intrinsically.
Browser Environment vs Node.js Environment
It is important to note that the environment described in this article is provided by the browser. Node.js provides a completely different runtime environment, though it is also powered by Google Chrome’s V8 JS Engine. This means Node.js will not provide for you the DOM tree, AJAX, or other Web API’s. However, you can install your preferred packages into the Node environment as needed for the program you are creating.
Here are some very important, and probably better resources than my article. After all, I learned most of how I understand all this from these resources…
Event Loop & Concurrency - Explained by Mozilla, using code examples
Another similar explanation - by Aseem Raj Baranwal
Lazy Parsing & Full Parsing - Is it worth it?
A more detailed explanation- More literal explanation