Skip to content

Repository files navigation

Writing Javascript Event Loop in typescript to understand and learn its inner workings, I will keep updating the notes below as I learn more.

Implemented

  • Only console.log('string') is supported for now through input.ts file which depicts a fake AST tree for demo puposes.

In Progress

  • I am working on implementing Promises.

Notes

Javascript Engine is very complex with multiple entities working together. I will try to break it down in as simple terms as possible.

How is Javascript code run ?

The javascript code we write is filled with keywords, special operators, and literals.

  • All of these things are picked line by line and converted into a nested JSON object also called Abstract Syntax Tree (AST) by an entity called javascript parser.
  • This big json object tree is converted to Bytecode (which is similar to register based code) by an entity called Interpreter. (just like java bytecode for JVM)
  • This bytecode is then turned into machine code (binary code) by an entity called Compiler.

Why is javascript converted into these codes multiple times, can we just not use javascript or machine code directly ?

  • Machines like your computer only understands 0 and 1 binary, it doesn't understand alphabets that is why it is converted to machine code.

  • If javascript code is converted to machine code directly it can take quite a few seconds for medium to large websites and user would have to stare at a blank screen for that time which can also vary depending on the sytem,

  • Instead because javascript is dynamically typed language it is converted to AST and to bytecode by interpreter which is quite fast, then bytecode is used to kickstart running the website and based on which code is running right now, bytecode decides what are the types of the variables being used and only current running code is converted to machine code with Just In Time Compiler (JIIT).

alt text

What is Event Loop ?

Javascript is a single threaded language, which means it can only run 1 thing at a time, be it user input, network calls, scrolling, etc. Going by the logic of single threaded language, if a user goes to a blog page until that blog is fetched from the server, user shouldn't be able to click, scroll or change the page, but this is not what happens in reality that is because of Event Loop which takes care of every task so the user does not wait.

Event Loop is a scheduler whose only job is to schedule which task to push into Call Stack based on few rules.

To understand event loop in detail we need to understand few entities:

CallStack: Every task which has to be executed has to be pushed inside callstack, which JS Engine executes 1 by 1. Every item pushed inside CallStack has its own execution context and its own memory space. Call stack follows Last In First Out.

MacroTask Queue: Standard scheduled and asynchronous Browser APIs and tasks are pushed to macrotask queue. Example - setTimeout, setInterval, Event Listeners Callbacks. Call stack follows First In First Out.

MicroTask Queue: Urgent and immediate asynchronous tasks are pushed to microtask queue. Example - Promise.then, Promise.catch, Promise.finally, queueMicrotask callbacks. Call stack follows First In First Out.

Now lets go through the sequence of how Event Loop runs:

Javascript Code is read line by line converted to machine code and decided what to do out of these 3 options.
  • If it is a synchronous code, push to Call Stack for JS Engine to execute it. (Example - console.log, function, let var declaration, etc.).
  • If it is an asynchronous browser API, they are pushed to MacroTask Queue. (Example - setTimeout, setInterval, Event Listener Callback).
  • If it is a javascript asynchronous API, they are pushed to MicroTask Queue. (Example - Promise callbacks, queueMicrotask, etc.).

This above loop is run until all synchronous code in the file are not executed, which means until all synchronous code is not executed all asynchronous APIs are waiting in MacroTask and MicroTask Queue to run.

When CallStack is empty after executing all sync code, event loop comes in the picture.

Example snippet from the EventLoop.ts file:

while (
  !callStack.isEmpty() ||
  !microtaskQueue.isEmpty() ||
  !macrotaskQueue.isEmpty()
) {
  while (!callStack.isEmpty()) {
    executeCallStack();
  }

  while (!microtaskQueue.isEmpty()) {
    const microTaskItem = microtaskQueue.remove();
    callStack.push(microTaskItem);

    while (!callStack.isEmpty()) {
      executeCallStack();
    }
  }

  if (!macrotaskQueue.isEmpty()) {
    const macroTaskItem = macrotaskQueue.remove();
    callStack.push(macroTaskItem);

    while (!callStack.isEmpty()) {
      executeCallStack();
    }
  }
}

Event Loop steps:

  1. Check if Call Stack is empty and not executing anything.
  2. Check if microtask queue has items if yes pop each task 1 by 1 and push to call stack and wait for execution, repeat until microtask queue and call stack empty.
  3. Check if macrotask queue has items, if yes pop 1 item and push to call stack.
  4. Go Back to STEP 1

Did you notice: Event Loop flushes whole microtask queue after 1 macrotask queue item.

alt text

Now this was half the picture, after reading through this I have few questions:

  1. If setTimeout has a delay after which it can be ran, who counts the time ? If javascript will count the time, because it is single threaded nothing else will work, same question for network calls, event listeners, how does all these tasks run, while the website works normally ?
  2. If a setTimeout is run and at the same time a network call is being made and a user clicks on a button, how are all these taken care of at the same time without hanging the app ?
  3. How does animations and UI paints on the website happen while all of these are happening at the same time, will this not make the animations and paints freeze ?

Now lets read about 2 more entities:

Browser APIs: APIs like setTimeout, fetch, I/O are provided by browser Chrome, Firefox, Safari, they are not native part of javascript, we just access those apis with javascript. So logically these APIs are also handled by these browsers, For Example:

  • When js engine comes across setTimeout with 5s delay, it is passed on to browser, then until those 5 seconds are elapsed browser's own C++ and Rust threads keep the count of the delay timer and as soon as the delay is elapsed the callback function is pushed to MacroTask Queue from where Event Loop takes over as discussed before.
  • Same logic goes for event listeners like onClick, onChange, when a mouse is hovered or clicked on an element Browser C++ threads check internally if that element had an event listener attached and if yes, its callback function is pushed to MacroTask Queue.

Request Animation Frame: Also called rAF,

Before talking about rAF, lets talk about 1 more phase inside Event Loop called Rendering phase: All monitors have refresh rate like 60Hz or 120Hz, refresh rate is the frequency by which changes are to be painted on the screen every second. Browsers like chrome check your system for refresh rate and decide the painting interval.

For example: If you have a monitor with refresh rate of 60Hz

$$ \text{Time (ms)} = \frac{1000}{60 (Hz)} = 16.6ms $$

with this formula browser decides that every 16.6ms the screen needs to be re-painted.

So rAF is another entirely different queue in the Event loop which has task for rendering/updating the UI and can be used with requestAnimationFrame(() => callback);;

So when Event Loop is running, browser's C++ threads are counting for next rendering phase (16.6ms in this case) and as soon as that hits, After CallStack is empty before picking MacroTask or MicroTask, Event Loop picks rAF queue and flushes out each task and runs them, and then does geometric calculation for figuring out the layout based on UI updates and then re-paints them on the screen.

Fun Fact: Event Loop takes a snapshot of current items inside rAF queue before starting to push them to call stack so that it does not execute callbacks created by current rAF tasks as they will be picked in the next rendering phase, which is totally opposite to microtasks in which case all callbacks created by current microtasks items are also executed.

So updated diagram would be something like:

alt text

Research

  • Watched Philip Roberts video on what the heck is event loop anyway ? from JSConf 2014.
  • Gone through this Loupe codebase to understand how he wrote it.
  • Read MDN docs to understand how exactly every entity works
  • Asked AI questions to clear doubts and know inner workings of v8.

About

Writing javascript event loop in typescript to understand and re-produce js internal workings

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages