Asynchronous JavaScript & EVENT LOOP from scratch

  • Browser has JS Engine which has Call Stack which has Global execution context, local execution context etc.

  • But browser has many other superpowers - Local storage space, Timer, place to enter URL, Bluetooth access, Geolocation access and so on

  • Now JS needs some way to connect the callstack with all these superpowers. This is done using Web APIs.
    1.setTimeOut()
    2.fetch()
    3.DOM API
    4.console.log
    5.LocalStorage
    and so many more.
    None of the above are part of Javascript! These are the extra superpowers that browser has. Browser gives access to JS callstack to use these powers.

  • We get all these inside call stack through global object ie. window (window.localStorage, window.setTimeout() and etc )

  • lik

      console.log("start");
      setTimeout(function cb() {
        console.log("timer");
      }, 5000);
      console.log("end");
    
      //output 
      start
      end
      // after 1 sec (1000 milliseconds)
      timer
    
    • First a GEC is created and put inside call stack.

    • console.log("Start"); // this calls the console web api (through window) which in turn actually modifies values in console.

    • setTimeout(function cb() { //this calls the setTimeout web api which gives access to timer feature. It stores the callback cb() and starts timer. console.log("Callback");}, 5000);

    • console.log("End"); // calls console api and logs in console window. After this GEC pops from call stack.

    • While all this is happening, the timer is constantly ticking. After it becomes 0, the callback cb() has to run.

    • Now we need this cb to go into call stack. Only then will it be executed. For this we need event loop and Callback queue

    • Event Loops and Callback Queue

    • cb() cannot simply directly go to callstack to be execeuted. It goes through the callback queue when timer expires.

    • Event loop keep checking the callback queue, and see if it has any element to puts it into call stack. It is like a gate keeper.

    • Once cb() is in callback queue, eventloop pushes it to callstack to run. Console API is used and log printed

      Event Loop: The event loop continuously checks the call stack and the task queue. If the call stack is empty, it takes the first task from the task queue and pushes it onto the call stack, where it's executed.

    • The event loop understands this and makes sure that the callbacks from the Callback Queue are added to the Call Stack, once the Call Stack is empty.

  • CALLBACK QUEUE is also known as, Message Queue or Event Queue.With this queue, we have the callbacks from event table. The functions are waiting to be dequeued and put on the Call Stack

  • Again Call back Queue has two components

    1.Micro Task Queue / message queue
    2.Macro Task Queue

    • 1.Macrotask or just called the task queue is code which needs to be executed after execution stack is empty. Macrotask are non blocking in nature. setInterval, setImmediate, setTimeout, I/O tasks, DOM Manipulation use the macrotask queue to run their callbacks

    • 2.Promises, MutationObservers, IntersectionObservers etc. use the microtask queue to run their callbacks.Microtask are kind of blocking in nature

    • The difference between the message queue(macro task) and the microtask queue is that the microtask queue has a higher priority than the message queue, which means that promise jobs inside the job queue/ micro-task queue will be executed before the callbacks inside the message queue.

      sync tasks → micro tasks → macro tasks

      1. All functions in that are currently in the call stack get executed. When they returned a value, they get popped off the stack
      1. When the call stack is empty, all queued up microtasks are popped onto the. call stack one by one, and get executed! (Microtasks themselves can also return new microtasks, effectively creating an infinite microtask loop ).Microstask also execute at the end of each macrotask
      1. If both the call stack and microtask queue are empty, the event loop checks if there are tasks left on the (macro)task queue. The tasks get popped onto the call stack, executed, and popped off!
    • Microtask are kind of blocking in nature. Unlike macrotasks, the main thread will be blocked until microtask queue is empty.So if you keep queuing tasks in the microtask queue forever, your browser will become unresponsive. You can’t click a button, can’t scroll, nothing…, GIFs stop, animations stop etc.

    • Macro tasks:

      • Macro tasks represent larger, more long-running operations such as I/O operations (like setTimeout, setInterval, XMLHttpRequest, fetch), DOM manipulation, and UI rendering.

      • They are scheduled by the browser's event loop and executed as separate units of work.

      • Macro tasks are typically used for tasks that are not time-sensitive and can be deferred until the call stack is empty.

    • Micro tasks:

      • Micro tasks represent smaller, more immediate tasks that need to be executed as soon as possible.

      • They are usually associated with promises and represent the resolution or rejection of a promise.

      • Micro tasks are executed after the current script has finished running but before the next event loop iteration.

      • Examples of micro tasks include promise callbacks (then, catch, finally) and queueMicrotask.

Here are some guidelines for segregating tasks into macro and micro tasks:

  • Use macro tasks for asynchronous operations that don't need to be executed immediately or are not time-sensitive, such as network requests (fetch), file I/O, and setTimeout/setInterval.

  • Use micro tasks for tasks that need to be executed as soon as possible after the current script has finished running, such as promise callbacks and other high-priority tasks.

  • Consider the timing and priority of the task when deciding whether to use a macro or micro task. Tasks that need to be executed immediately or with high priority should be scheduled as micro tasks.

By understanding the differences between macro and micro tasks and applying them appropriately in your code, you can better manage the asynchronous behavior of your applications and ensure smooth and efficient execution.

  • Example Explain

  •   console.log('Start');
    
      setTimeout(() => {
        console.log('setTimeout');
      }, 0);
    
      Promise.resolve().then(() => {
        console.log('Promise');
      });
    
      console.log('End');
    
      // Output
      Start
      End
      Promise
      setTimeout
    

    fig 4.1 how promises work

  • Refer fig 4.1-On the first line, the engine encounters the console method. It gets added to the call stack, after which it logs the value start! to the console. The method gets popped off the call stack, and the engine continues.

  • fig 4.2 how promises work

  • Refer fig 4.2-The engine encounters the setTimeout , which gets popped on to the call stack.its callback function will get added to the Web API, until the timer is done. Although we provided the value 0 for the timer, the call back still gets pushed to the Web API first, after which it gets added to the macrotask queue

fig 4.3 how promises work

  • Refer fig 4.3-The engine encounters the promise method. The promise method gets added to the call stack then its callback function will get added to the Web API . when promise resolve, its callback function gets added to the microtask queue

fig 4.4 how promises work

  • Refer fig 4.4-The engine encounters the console method. It gets added to the call stack immediately, after which it logs the value End! to the console, gets popped off the call stack, and the engine continues.

fig 4.5 how promises work

  • Refer fig 4.5-The engine sees the call stack is empty now. it’s going to check whether there are queued tasks in the microtask queue! And yes there are, the promise then callback is waiting for its turn! It gets popped onto the call stack, after which it logs the resolved value of the promise.

fig 4.6 how promises work

  • Refer fig 4.6-The engine sees the call stack is empty, so it’s going to check the microtask queue once again to see if tasks are queued. Nope, the microtask queue is all empty.The setTimeout callback gets popped on to the call stack. The callback function returns the console method.The setTimeout callback get popped off the call stack.

Conclusion

So we have learned how asynchronous JavaScript works and other concepts such as call stack, event loop, message queue/task queue and job queue/micro-task queue which together make the JavaScript runtime environment.

That’s it and if you found this article helpful, please click the like button, and if you have any doubt, feel free to comment! I’d be happy to help