Event-loop, callstack and async JS

Event-loop, callstack and async JS

Thread in JS

Thread in computer science is the execution of running multiple tasks or programs at the same time. Each unit capable of executing code is called a thread.

The main thread is the one used by the browser to handle user events, render and paint the display, and to run the majority of the code that comprises a typical web page or app. Because these things are all happening in one thread, a slow website or app script slows down the entire browser; worse, if a site or app script enters an infinite loop, the entire browser will hang. This results in a frustrating, sluggish (or worse) user experience.

Process and Thread in Chrome

Chrome architecture has two main processes Browser Process and Renderer Process. Browser UI is running in Browser Process and When you open new tab in Chrome browser, for each tab a new process is gets created called as Renderer Process. Renderer process handles rendering of your HTML contents.

Suppose you have opened 10 tabs in your chrome which leads to creation of 10 Renderer process and 1 browser process.

Why separate Renderer process for each tab ?

Suppose at any point of time while browsing through internet because of some reason some tab crashed, then only that renderer process gets killed and other process are still alive. Your 9 tabs are still responsive and working. As browser UI is running in different process. Browser UI wont get hanged in general and its responsive enough. As each renderer is running as different process, shared data access is difficult(thread can access shared data) which provides inter tab data security. There are many other process running on and having there own purpose. Firefox has some different architecture compared to chrome.

However, modern JavaScript offers ways to create additional threads, each executing independently while possibly communicating between one another. This is done using technologies such as web workers, which can be used to spin off a sub-program which runs concurrently with the main thread in a thread of its own. This allows slow, complex, or long-running tasks to be executed independently of the main thread, preserving the overall performance of the site or app—as well as that of the browser overall. This also allows individuals to take advantage of modern multi-core processors.

A special type of worker, called a service worker, can be created which can be left behind by a site—with the user's permission—to run even when the user isn't currently using that site. This is used to create sites capable of notifying the user when things happen while they're not actively engaged with a site.

Event Loop

Each 'thread' gets its own event loop, so each web worker gets its own, so it can execute independently, whereas all windows on the same origin share an event loop as they can synchronously communicate.

The event loop runs continually, executing any tasks queued. An event loop has multiple task sources which guarantees execution order within that source (specs such as IndexedDB define their own), but the browser gets to pick which source to take a task from on each turn of the loop. This allows the browser to give preference to performance sensitive tasks such as user-input.

Tasks are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, or setting a setTimeout.

Task vs Microtask

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 the 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.
  • The event loop driving your code handles these tasks one after another, in the order in which they were enqueued. The oldest runnable task in the task queue will be executed during a single iteration of the event loop. After that, microtasks will be executed until the microtask queue is empty, and then the browser may choose to update rendering. Then the browser moves on to the next iteration of event loop.

Note - A JS script ( a program )is also put into task queue at first before being run by js engine. After that rest all tasks and microstasks get chance to run.

Microtask

At first the difference between microtask and task seems minor. And they are similar; both are made up of JavaScript code which gets placed on a queue and run at an appropriate time. However, whereas the event loop runs only the tasks present on the queue when the iteration began, one after another, it handles the microtask queue very differently.

  • Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

  • The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of execution of each task by JS engine. Any additional microtasks queued during microtasks are added to the end of the queue and also processed (by calling queueMicrotask()).

Microtasks include mutation observer callbacks and promise callbacks. Once a promise settles, or if it has already settled, it queues a microtask for its reactionary callbacks. This ensures promise callbacks are async even if the promise has already settled.

In summary:

  • Tasks execute in order, and the browser may render between them
  • Microtasks execute in order, and are executed:
  • After every callback, as long as no other JavaScript is mid-execution and at the end of each task.
  • Microtask has higher priority over task as it is best to render any major change before handling events in browser.

Check this link for more detail understanding of event loop and microtask queue.

In the article you will also find the difference in behaviour of event loop in different browsers.

Event Loop

image.png

function foo(b) {
  let a = 10
  return a + b + 11
}

function bar(x) {
  let y = 3
  return foo(x * y)
}

const baz = bar(7) // assigns 42 to baz

Call Stack

Order of operations:

  • When calling bar, a first frame is created containing references to bar's arguments and local variables.
  • When bar calls foo, a second frame is created and pushed on top of the first one, containing references to foo's arguments and local variables.
  • When foo returns, the top frame element is popped out of the stack (leaving only bar's call frame).
  • When bar returns, the stack is empty.
  • The arguments and local variables may continue to exist, as they are stored outside the stack — so they can be accessed by any nested functions long after their outer function has returned.

Heap

Objects are allocated in a heap which is just a name to denote a large (mostly unstructured) region of memory.

Queue

A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message.

At some point during the event loop, the runtime starts handling the messages on the queue, starting with the oldest one. To do so, the message is removed from the queue and its corresponding function is called with the message as an input parameter. As always, calling a function creates a new stack frame for that function's use.

The processing of functions continues until the stack is once again empty. Then, the event loop will process the next message in the queue (if there is one).

Adding messages

In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost. So a click on an element with a click event handler will add a message—likewise with any other event.

The function setTimeout is called with 2 arguments: a message to add to the queue, and a time value (optional; defaults to 0). The time value represents the (minimum) delay after which the message will be pushed into the queue.

If there is no other message in the queue, and the stack is empty, the message is processed right after the delay. However, if there are messages, the setTimeout message will have to wait for other messages to be processed. For this reason, the second argument indicates a minimum time—not a guaranteed time.

Zero delays

Zero delay doesn't mean the call back will fire-off after zero milliseconds. Calling setTimeout with a delay of 0 (zero) milliseconds doesn't execute the callback function after the given interval.

The execution depends on the number of waiting tasks in the queue. In the example below, the message "this is just a message" will be written to the console before the message in the callback gets processed, because the delay is the minimum time required for the runtime to process the request (not a guaranteed time).

Callbacks

Important questions:

  • Learn how setInterval works
  • write a function which takes a message and time. The function should print that message every X interval.
  • Write a function that takes a number. Then print a countdown from that number to 0. At zero print "Bang Bang!". The important question is sometimes asked in FAANG interviews as well.
  • Create a button in React and print the event
  • Can you print the button text from this event?
  • Create a list in React. Use array of objects. Use map to render the list
  • On every list there should be an onClick handler. Clicking on this should print the details of the object.

Promise vs Callback

Promises have some similarities to old-style callbacks. They are essentially a returned object to which you attach callback functions, rather than having to pass callbacks into a function.

However, promises are specifically made for handling async operations, and have many advantages over old-style callbacks:

You can chain multiple async operations together using multiple .then() operations, passing the result of one into the next one as an input. This is much harder to do with callbacks, which often ends up with a messy "pyramid of doom" (also known as callback hell).

Promise callbacks are always called in the strict order they are placed in the event queue. Error handling is much better — all errors are handled by a single .catch() block at the end of the block, rather than being individually handled in each level of the "pyramid".

Promises avoid inversion of control, unlike old-style callbacks, which lose full control of how the function will be executed when passing a callback to a third-party library.

Check this collection of questions on promises, CodeSandbox - Promise.

Asynchronous vs Parallelism vs Concurrency

Stack overflow