Run, JavaScript, Run

Auteur(s) de l'article

JavaScript is a single-threaded non-blocking programming language. I have heard this since I started coding, without fully understanding what it means. By looking into the Event Loop Model and the notion of tasks, the idea is to better understand those notions.

The Event Loop Model

An application or website coded in JavaScript runs on one unique thread, sharing its Event Loop. The Event Loop is responsible for scheduling and executing the different tasks (= piece of code) within its thread and is composed of three elements:
  1. The Call Stack (or Execution Stack) which handles the synchronous calls and executes the code one at a time.
  2. The Web APIs are asynchronous calls which are triggered by an action (for example, on a click, or when a timer ends)
  3. The Callback Queue which handles the asynchronous calls and can be divided into two different queues, the Job Queue and the Task Queue.
    So what does the loop do ?
    It continuously looks into the Call Stack for some code to execute. As soon as the call stack is empty, it then looks into the Queue for some tasks to dispatch into the Call Stack and execute. So, its main role is to connect the Call Stack with the Queue.

    2 types of queues

    So, why is there 2 types of queues to handle asynchronous tasks ? This is a matter of priority.
    The Job Queue (also called Micro tasks Queue) handles all high priority async operations. A few examples are:
    Promises, processes.nextTick, queueMicrotask, etc.
    The Task Queue (also called Macro task Queue) handles lower priority async operations. A few examples are:
    setTimeout, setInterval, setImmediate, etc.
    Whenever the Call Stack is empty, the Event Loop comes first to look into the Job Queue for the next task to handle. As long as there are tasks in that queue, it will keep picking from it.
    This can lead to some unwanted infinite loop, because a micro task can return another micro task, which can return another task, which can ... You see what I'm getting at, so just be careful.
    Once the Job Queue is empty, it’s the Task Queue turn. The Event Loop takes the top task of that queue and dispatches it to the Call Stack for execution. Once the iteration is done, it starts over, looking first to the Call Stack, then to the Job Queue for any other available tasks, before coming back to the Task Queue.
    To summarize:
    • A loop iteration will first execute all the tasks from the Job Queue, before looking to execute one task from the Task Queue and finishing its iteration. The tasks stacked in the Job queue will be handled multiple times per loop iteration, while only one task of the Task queue will be handle during an iteration.
    • As long as there are tasks inside the Job Queue, the loop will continue processing those operations, before executing any task of the Task Queue. 
      console.log('1st message');
      
      setTimeout(() => {
         console.log('2nd message: setTimeout');
      }, 0);
      
      const promise = new Promise((resolve, reject) => {
         resolve();
      });
      promise.then(resolve => {
         console.log('3rd message: promise');
      })
      
      console.log('4th message');
      If you run the code above, the printed result will be:
      "1st message"
      "4th message"
      "3rd message: promise"
      "2nd message: setTimeout"
      The console.log functions are immediately present in the Call Stack and are the first to be executed. Then, as the Job Queue takes priority, it is the 2 Promise functions that are executed. Finally, because the Job Queue is empty, it is the setTimeout function turn to be executed.

      Conclusion

      Most of the time, there is no need to handle any particular Micro tasks. But it can help when trying to improve performances: for example, if we are writing a very heavy computation function, it might be interesting to chunk it in different pieces and handle it asynchronously, to prevent unresponsive browsers.
      But we also need to be aware of the issues that can arise:
      • We might cause an infinite loop
      • Since a Micro task can queue other Micro tasks, it can take a very long time until the loop processes the next Macro task, resulting in a blocked UI for example.

        Some sources