The Event Loop is a fundamental concept in Node.js that allows it to handle asynchronous operations efficiently. Understanding how the Event Loop works is crucial for grasping how Node.js manages non-blocking I/O operations and concurrency.
Overview of the Event Loop
Node.js is single-threaded, meaning it operates on a single thread of execution. However, it can handle multiple operations concurrently thanks to the Event Loop, which enables asynchronous, non-blocking I/O operations. This design makes Node.js particularly well-suited for I/O-bound tasks like web servers, where handling many simultaneous connections efficiently is critical.
How the Event Loop Works?
1. Call Stack:
This is where function calls are executed one at a time. JavaScript is single-threaded, so only one operation can occur at a time in the call stack.
2. Node APIs/Thread Pool:
When an asynchronous operation (like I/O, timers, or network requests) is invoked, it’s offloaded to the Node.js APIs, which might leverage a thread pool to handle the operation in the background.
3. Callback Queue (Task Queue):
Once an asynchronous operation is completed, its callback is placed in the Callback Queue. These callbacks are functions that should be executed once the result of an asynchronous operation is ready.
4. Event Loop:
The Event Loop continuously monitors the Call Stack and the Callback Queue.
If the Call Stack is empty, the Event Loop picks the first callback from the Callback Queue and pushes it to the Call Stack for execution.
This cycle repeats, enabling Node.js to handle asynchronous operations without blocking the main thread.
Phases of the Event Loop
The Event Loop operates in several phases, where each phase handles different types of callbacks:
1. Timers Phase:
Handles callbacks from setTimeout()
and setInterval()
once the specified time has elapsed.
2. I/O Callbacks Phase:
Processes callbacks from some system operations like errors or connection completion callbacks (excluding file, network, and setTimeout
).
3. Idle, Prepare Phase:
Internal use only. Prepare handles callbacks executed right before the poll phase.
4. Poll Phase:
Retrieves new I/O events and executes I/O-related callbacks. It also manages the blocking of the event loop when no tasks are queued.
5. Check Phase:
Handles callbacks from setImmediate()
. These callbacks are executed after the poll phase.
6. Close Callbacks Phase:
Executes callbacks for close
events, such as when a socket or handle is closed.
Example to Illustrate the Event Loop
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
setImmediate(() => {
console.log('Immediate');
});
console.log('End');
Output:
StartEnd
Immediate
Timeout
Explanation:
The console.log('Start')
and console.log('End')
execute immediately because they are synchronous.
setTimeout(() => {...}, 0)
schedules the callback in the Timer phase but waits until the next tick of the event loop.
setImmediate(() => {...})
schedules the callback for the Check phase.
Since the Event Loop checks the setImmediate
queue before it checks the Timer queue, “Immediate” is logged before “Timeout”.
An event loop has One Process, One Thread and One Event Loop.
Now, let’s take a look at how our code runs inside of the Node.js instance.
console.log('Task 1');
console.log('Task 2');
// some time consuming for loop
for(let i = 0; i < 999999; i++) {
}
console.log('Task 3');
What happens when we run this code? It will first print out Task 1 than Task 2 and then it will run the time consuming by the for a loop after that it will print out Task 3.
Node puts all our tasks into an Events queue and sends them one by one to the event loop. The event loop is single-threaded and it can only run one thing at a time. So it goes through Task 1 and Task 2 then the very big for loop and then it goes to Task 3. This is why we see a pause in the terminal after Task 2 because it is running the for a loop.
Now let’s do something different. Let’s replace that for loop with an I/O event.
console.log('Task 1');
console.log('Task 2');
fs.readFile('./file.txt', (err, data) => {
if (err) throw err;
console.log('done reading file');
process.exit();
});
console.log('Task 3');
Now the result is
Task 1
Task 2
Task 3
done reading file
I/O tasks, network requests, database processes are classified as blocking tasks in Node.js. So whenever the event loop encounters these tasks it sends them off to a different thread and moves on to the next task in events queue. A thread gets initiated from the thread pool to handle each blocking tasks and when it is done, it puts the result in a call-back queue. When the event loop is done executing everything in the events queue it will start executing the tasks in the call-back queue.