How the Event Loop Works: What, Why, and How Explained
Before we jump onto how the event loop works , We will discuss on what Event loop is about
The event loop is a core component of JavaScript's runtime environment that allows for asynchronous, non-blocking operations despite JavaScript being single-threaded. It manages the execution of code, the collection and processing of events, and the execution of queued sub-tasks.
I understand that the whole definition would have felt like a mouthful of words , “Queued subtasks” , “runtime environment” etc etc
So here is the thing,
Javascript is a single threaded language , it means that JavaScript executes code in a single sequence or one thread of execution. This thread can handle only one operation at a time. Here's what that implies:
- One Operation at a Time:
JavaScript can execute only one piece of code at any given moment. This is often referred to as "synchronous" execution because each operation must complete before the next one starts.
- Call Stack
JavaScript maintains a call stack, which is a data structure that keeps track of function calls. When a function is called, it's added to the top of the call stack. When the function returns, it's removed from the stack.
- Blocking Nature
If a task takes a long time to complete (e.g., a large computation or a blocking I/O operation), it can block the entire thread, preventing other tasks from executing until the current one finishes.
Now if javascript has a blocking nature, How are we able to do asynchronous calls without feeling delayed on most JS apps ?
The answer lies in how ability of javascript to handle asynchronous tasks
How JavaScript Handles Asynchronous Tasks:
Despite being single-threaded, JavaScript can handle asynchronous tasks (like network requests, file I/O, timers) without blocking the main execution thread. This is achieved through the event loop, which allows JavaScript to offload certain tasks and execute them later, enabling non-blocking behavior.
The above image denotes the visual representation of the runtime concepts which will help to understand the event loop in a better way
- Call Stack:
A stack data structure that keeps track of function calls. When a function is called, it gets added (pushed) to the top of the stack. When the function completes, it is removed (popped) from the stack.
- Web APIs
Browsers provide Web APIs (like setTimeout, DOM events, fetch, etc.) for performing asynchronous operations. When you call an asynchronous function, it is handled by these Web APIs.
- Callback Queue (Task Queue)
Callback Queue is a queue that holds messages (functions or callbacks) to be processed. When an asynchronous operation completes, its callback is added to this queue.
The microtask queue is a queue where microtasks are placed. Microtasks are typically scheduled by promises (e.g., .then or .catch methods), MutationObserver callbacks, and other similar APIs. Microtasks are intended to run after the currently executing script and before any other macrotasks.
The macrotask queue, also simply known as the task queue, is a queue where macrotasks are placed. Macrotasks are typically scheduled by functions like setTimeout, setInterval, setImmediate (in Node.js), and certain DOM events. Each time the event loop executes a macrotask, it will empty the entire microtask queue before executing the next macrotask.
- Event Loop
A mechanism that continuously checks the call stack and the callback queue. If the call stack is empty, it will take the first callback from the callback queue and push it onto the call stack for execution.
- Heap
The heap is a region of memory where JavaScript allocates objects, functions, and other reference types. Unlike the call stack, which operates in a Last-In-First-Out (LIFO) manner, the heap does not follow a specific order for allocating or deallocating memory. This allows for the dynamic allocation of memory for objects and functions at runtime.
Code Example
console.log('Start'); // Step 1
setTimeout(() => { // Macro-task
console.log('Timeout');
}, 0);
Promise.resolve().then(() => { // Micro-task
console.log('Promise 1');
}).then(() => { // Micro-task
console.log('Promise 2');
});
const obj = { name: 'Divyanshu' }; // Stored in the heap
console.log('End'); // Step 2
Step by Step Execution
- console.log('Start'):
- Call Stack: console.log('Start')
- Executes immediately, printing Start.
- Call Stack: Empty after execution.
- setTimeout Call :
- Call Stack: setTimeout
- Schedules a callback to be executed after 0 milliseconds.
- Web API: The setTimeout function hands off the timer to the Web API.
- Call Stack: Empty after setTimeout function call completes.
- Promise.resolve().then(...) :
- Call Stack: Promise.resolve().then
- Schedules the first .then callback as a micro-task.
- Micro-task Queue: Adds the first .then callback (Micro-task 1).
- Call Stack: Empty after Promise.resolve().then(...) call completes.
- Object Creation
- Call Stack: const obj = { name: 'Divyanshu' }`
- Creates an object and stores it in the heap.
- Call Stack: Empty after object creation.
- (console.log('End')):
- Call Stack: console.log('End')
- Executes immediately, printing End.
- Call Stack: Empty after execution.
- Currently , there is one item in Microtask Queue and one item in Macrotask queue waiting to be executed
- Micro-task Queue :
- The event loop first checks the micro-task queue.
- Micro-task Queue: console.log('Promise 1')
- Call Stack: console.log('Promise 1')
- Executes, printing Promise 1.
- Micro-task Queue: Adds the second .then callback (Micro-task 2).
- Call Stack in empty after execution
- Micro-task Queue: console.log('Promise 2')
- Call Stack: console.log('Promise 2')
- Executes, printing Promise 2.
- Call Stack: Empty after execution completes.
- Macro-task Queue:
- Macro-task Queue: console.log('Timeout')
- Call Stack: console.log('Timeout')
- Executes, printing Timeout.
- Call Stack: Empty after execution.
Final Output
Start
End
Promise 1
Promise 2
Timeout
Detailed Explanation
- Initial Synchronous Execution
- console.log('Start') is executed first, printing Start.
- setTimeout schedules a callback to be executed after 0 milliseconds. This scheduling is handled by the Web API, and the callback is placed in the macro-task queue.
- Promise.resolve().then(...) schedules the first .then callback as a micro-task, which is placed in the micro-task queue.
- Object is created and stored in the heap.
- console.log('End') is executed next, printing End.
- Event Loop Processing:
- Micro-task Queue: After the initial synchronous code execution, the event loop checks the micro-task queue and executes all the micro-tasks before moving to the macro-task queue.
- console.log('Promise 1') is executed first, printing Promise 1. The second .then callback is added to the micro-task queue.
- console.log('Promise 2') is executed next, printing Promise 2.
- Macro-task Queue: After the micro-task queue is empty, the event loop moves to the macro-task queue and executes the scheduled callback.
- console.log('Timeout') is executed, printing Timeout.
- Micro-task Queue: After the initial synchronous code execution, the event loop checks the micro-task queue and executes all the micro-tasks before moving to the macro-task queue.
As you can see Event Loop is something which makes sure that javascript being a single threaded language can run asynchronous code efficiently.
Microtask Queue and Macrotask Queue
The macrotask queue, also simply known as the task queue, is a queue where macrotasks are placed. Macrotasks are typically scheduled by functions like setTimeout, setInterval, setImmediate (in Node.js), and certain DOM events. Each time the event loop executes a macrotask, it will empty the entire microtask queue before executing the next macrotask.
The microtask queue is a queue where microtasks are placed. Microtasks are typically scheduled by promises (e.g., .then or .catch methods), MutationObserver callbacks, and other similar APIs. Microtasks are intended to run after the currently executing script and before any other macrotasks.
Key Differences between them
- Execution Priority:
- Microtasks: Have higher priority. The event loop will always check and execute all microtasks in the microtask queue before moving on to the next macrotask.
- Macrotasks: Execute in the order they are placed in the macrotask queue, but only after the current microtask queue is emptied.
- Use Cases:
- Microtasks: Used for tasks that should happen as soon as possible but after the currently executing script completes. Commonly used for promise resolutions and DOM mutations.
- Macrotasks: Used for tasks that can be scheduled to run later, like timers and rendering events.
- Event Loop Interaction:
- After each macrotask, the event loop will empty the entire microtask queue before moving on to the next macrotask.
Summary
- The call stack processes each function call synchronously. The call stack acts as a central processing unit where every function call initially goes. Based on the nature of the function, it either remains on the call stack until execution completes, or it delegates further processing to other components like Web APIs.
- setTimeout and other asynchronous functions offload tasks to Web APIs, which schedule them in the macro-task or micro-task queue.
- The event loop ensures that all micro-tasks are executed before moving on to the next macro-task.
- The heap is used to store objects and functions, but it is not directly involved in the event loop or task scheduling.