All Articles

đŸ„Š Cracking JavaScript Series: Event Loop

We may have heard that JavaScript executes all the operations on a single thread, but what is the magic empowering JavaScript to be an asynchronous programming language? Event Loop is the rationale and forms the non-blocking feature like Node.js.

First, we must keep in mind that Event Loop is related to the runtime environment, such as the browser or Node.js, not the JavaScript Engine(V8).

To be more specific, Event Loop is an endless loop that waits for tasks, executes them, and then sleeps, waiting for more tasks. It ensures all the tasks from WebAPIs(Browser) or C++ APIs(Node.js) could queue up for being pulled into the stack and finally become executed. If you first get to know this topic and still feel a bit fuzzy, don’t worry, I will use the following graph to explain. đŸ€“

Basic Architecture

js event loop

To execute an async function, the browser would utilize the four vital steps:

  1. Add a function into Call Stack.
  2. Handle the browser-related method in WebAPIs.
  3. Queue all the tasks in Task Queue.
  4. Pull a task to Call Stack one at a time by Event Loop till the queue is empty.

PS: To make the whole story short, we only focus on the “Stack” in the first step. If you are interested in how the V8 JavaScript Engine works, you could check out the previous article đŸ„Š Cracking JavaScript Series: JavaScript Engine.

Call Stack

It represents the order of JavaScript code execution and keeps track of its place in a script that calls multiple functions. Whenever a function is finished, it is popped from the stack. It is a LIFO queue (Last In, First Out).

  • Enqueue
  • Dequeue By the way, an anonymous function is the default function executed first in the Call Stack. It usually appears as a main() or global() function in the dev-tool of the browser.
  • Whenever an error occurs, the browser would also show its stack position.
  • Stack Overflow Some may ask: “how many recursive function calls causes stack overflow?” There is no standard answer, and it depends on your environment and function complexity. Here on OS X 10.13, I’m limited to 8192K by default.

WebAPIs

Since V8 engine does not include “setTimeout”, how does the browser execute it?

JavaScript did not implement the APIs for web operations, but the browser did, and it could be divided into Browser Object Model (BOM) and Document Object Model (DOM). Therefore, a standard JavaScript would contain the following three components:

  1. JavaScript Core (based on ECMAScript standard)
  2. BOM (Browser Object Model)
  3. DOM (Document Object Model)

For example, setTimeout() method is defined on the WindowOrWorkerGlobalScope mixin(interface) and implemented by Window and WorkerGlobalScope. Those implementations belong to Web Workers API so that we could use window.setTimout() or directly call setTimout() on our browsers. The followings are other methods that you may be familiar with:

  • WindowOrWorkerGlobalScope.atob()
  • WindowOrWorkerGlobalScope.btoa()
  • WindowOrWorkerGlobalScope.clearInterval()
  • WindowOrWorkerGlobalScope.createImageBitmap()
  • WindowOrWorkerGlobalScope.fetch()
  • WindowOrWorkerGlobalScope.queueMicrotask()
  • WindowOrWorkerGlobalScope.setInterval()

Task Queue

Task Queue could be the most complex but interesting field of asynchronous execution concept. For a better understanding, I would break Task Queue into three parts to elaborate.

  1. Definition of Task
  2. Advanced: Macrotask and Microtask
  3. Advanced: Odd Case of the combination of Macrotasks and Microtasks.

If you want to get a quick answer, part 1 would be enough, but if you still have time, I would suggest you dive into part2-3. :)

Definition of Task

A task is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. These all get scheduled on the task queue.

Tasks get added to the task queue when:

  • A new JavaScript program or subprogram is executed (such as from a console, or by running the code in a <script> element) directly.
  • An event fires, adding the event’s callback function to the task queue.
  • A timeout or interval created with setTimeout() or setInterval() is reached, causing the corresponding callback to be added to the task queue.

Keep in mind that the functions in the callback queue start getting executed only after:

  • We have reached the end of the program
  • There are no functions left to be executed in the call stack

Here comes an example:

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");

bar();
foo();
baz();

What’s the output?

"First"
"Third"
"Second"
  1. We invoke bar. bar returns a setTimeout function.
  2. The callback we passed to setTimeout gets added to the Web API, the setTimeout function and bar get popped off the callstack.
  3. The timer runs, in the meantime foo gets invoked and logs First. foo returns (undefined),baz gets invoked, and the callback gets added to the queue.
  4. baz logs Third. The event loop sees the callstack is empty after baz returned, after which the callback gets added to the call stack.
  5. The callback logs Second.

Basically, the setTimeout needs to wait for all the code for queued messages to complete even though you specified a particular time limit for your setTimeout.

The diagram is identical for Node.js, instead of WebAPIs, we have in C++ APIs and the threading is hidden from you by C++.

If we bind a click listener on a dom, it would be hooked on the WebAPIs and wait for triggering.

If we click too many times, the listener functions would be added to the callback queue, and whenever the call stack is empty, the event loop would pull another task from the callback queue to add it to the call stack for executing the code.

The event loop proceeds to execute all the callbacks waiting in the task queue. Inside the task queue, the tasks are broadly classified into two categories, namely micro-tasks and macro-tasks.

Macrotask and Microtask

The previous section explains the order between sync and async function.

Likewise, the browser also prioritizes async functions according to two task types: Macrotask and Microtask.

Macrotasks

  • Macro-task represents some discrete and independent work. These are always the execution of the JavaScript code and micro-task queue is empty. Macro-task queue is often considered the same as the task queue or the event queue. However, the only small difference between the two is that the task queue is used for synchronous statements whereas the macro-task queue is used for asynchronous statements.
  • In JavaScript, no code is allowed to execute until an event has occurred. It is worth mentioning that the execution of a JavaScript code execution is itself a macro-task. The event is queued as a macro-task. When a (macro) task, present in the macro-task queue is being executed, new events may be registered and in turn, created and added to the queue.
  • In comparison, the macro-task queue has a lower priority. Macro-tasks include parsing HTML, generating DOM, executing main thread JavaScript code, and other events such as page loading, input, network events, timer events, etc.

Examples: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI Rendering

Microtasks

  • A micro-task is said to be a function that is executed after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop being used by the user agent to drive the script’s execution environment. A Micro-task is also capable of en-queuing other micro-tasks.
  • Micro-tasks are often scheduled for things that are required to be completed immediately after the execution of the current script. On completion of one macro-task, the event loop moves on to the micro-task queue. The event loop does not move to the next task outside of the micro-task queue until all the tasks inside the micro-task queue are completed. This implies that the micro-task queue has a higher priority.
  • Once all the tasks inside the micro-task queue are finished, only then does the event loop shift back to the macro-task queue. The primary reason for prioritizing the micro-task queue is to improve the user experience. The micro-task queue is processed after callbacks given that any other JavaScript is not under mid-execution. Micro-tasks include mutation observer callbacks as well as promise callbacks.

Examples: process.nextTick, Promises, queueMicrotask, MutationObserver

What’s the output of the following code:

setTimeout(function a(){console.log("timeout")});

Promise.resolve()
  .then(function b(){console.log("promise")});

console.log("global ex. context");

online visualization

The answer would be:

"global ex. contenxt"
"promise"
"timeout"

task queue composition
The recap of Macrotask & Microtask

However, in the real scenario, nuance always exists and it may not match our understanding of the rule “Micro Queue is prior than the Marco Queue.”

Odd Case of the combination of Macrotasks and Microtasks

function display(data){console.log("Fetch resolved!")}
function printHello(){console.log("Callback Time")}
function blockExecution() {
  console.log("Blocking execution...");
  alert("Dont close the alert until 2000ms");
}

const futureData = fetch("https://jsonplaceholder.typicode.com/todos/1");
futureData.then(response => response.json()).then(display);
setTimeout(printHello, 2000);

const p = new Promise(
    (resolve, reject) => {
        console.log("I'm making a promise...");
        resolve("Promise resolved!");
        console.log("Promise is made...");
    });

p.then(
    // this is called the success handler
    result => console.log(result)
);

blockExecution();

console.log("Execution ended!");
  • Q: What’s the output?
I'm making a promise...
Promise is made...
Blocking execution...

1. Me first !
   hi back from fetch
   hello
   actual output :

2. Me first !
   hello
   hi back from fetch
  • A: Both 1, 2 could be the answer!!

❗Note: If the blockExecution() function block the thread long enough (over 2000ms), the printHello() function in the “Marco Queue” would be earlier pulled to the “Call Stack” than the display() function in the “Micro Queue”, so the answer would be:

I'm making a promise...
Promise is made...
Blocking execution...
Execution ended!
Promise resolved!
------
Callback Time
Fetch resolved!
  • Vice versa, if the blockExecution() function does not block the thread long enough(below 2000ms), the result would be:
I'm making a promise...
Promise is made...
Blocking execution...
Execution ended!
Promise resolved!
------
Fetch resolved!
Callback Time
  • Surprisingly, it seems like the first circumstance violates the rule. Nevertheless, both cases strongly prove the truth of the rule. Remember, the “Event Loop” would always try to fill the call stack whenever the stack is empty. If a Microtask does not even finish its job (get a response from fetch()), and the Macrotask has completed its work (setTimeout() get executed as soon as the delay time reach 2000ms), the Event Loop would pull the Macrotask first to the call stack. What a workaholic machine, right?😅.

How to define a “Blocking” code?

When people say don’t block the event loop, it’s saying don’t put shitty slow code on the call stack because when you do that, the browser can not create a nice fluid UI.

  • Run the blocking code synchronously. (forEach)
  • Run the blocking code asynchronously. (setTimeout)
  • Since the render queue get a higher priority than the callback queue (repaint in every 16.6 milliseconds: 60 frames/ per second), the difference between the two is that the asynchronous method gives render a chance within each element.

Conclusion

We have learned about the JavaScript event loop which is a constantly running process that coordinates the tasks between the call stack and callback queue to achieve concurrency.

Reference

  1. Philip Roberts | JSConf EU
  2. https://levelup.gitconnected.com/asynchronous-javascript-part-3-85390632dd1a
  3. https://towardsdev.com/event-loop-in-javascript-672c07618dc9