JavaScript is known for being a single threaded, asynchronous runtime. This is what makes it a great fit for high I/O servers, allowing Node to serve large numbers of concurrent connections with little memory or CPU overhead. But how does it really work?
Programs have two fundamental memory regions:
- Heap - This is where data is stored, especially dynamic data.
- Call Stack - A LIFO (Last In, First Out) stack of function calls.
The call stack being LIFO means functions at the top of the stack are returned first. The last item put in, is the first out. …but there’s more to it. We’re going to talk about how callbacks move from the event loop’s queues onto the call stack, where they’re executed.
Microtasks (The high priority VIPs)
First you have the VIP microtasks queue, comprised of two subqueues.
nextTick- Promise callbacks and
queueMicrotask
The microtasks queue is drained (meaning every item in the queue is executed) between each phase. In both the timers and the check phases, it also drains after each item in the queue.
For this same reason, recursive calls to nextTick can “starve” the loop. If nextTick callbacks are continually added to the microtasks queue, the queue will never become to allow the loop to move on to the next phase.
The Event Loop
JavaScript is single-threaded, all tasks are managed by the event loop. In Node the event loop is orchestrated by the libuv library. It powers the asynchronous, non-blocking I/O model of Node, managing asynchronous function calls and interacting with the operating system’s APIs.
There are 6 phases to the event loop:
The 6 phases
- Timers -
setTimeoutandsetInterval. Fires any timer callbacks whose delay has passed. Note: Timer delays aren’t the time until the callback runs, but rather the minimum time until the callback runs. - Pending callbacks - Callbacks deferred to the next loop iteration. Some system operations, such as TCP errors, want to wait to report their errors.
- Idle, Prepare - Node.js internals. This isn’t a phase you will interact with as a user.
- Poll - The core of the loop - processes I/O tasks and HTTP requests. This is also the phase where Node.js blocks (waits) when there is nothing to do.
- Check -
setImmediatecallbacks. Within an I/O callback,setImmediatealways runs beforesetTimeout. Outside of an I/O callback their order is not determined. - Close - “close” events like
socket.on(’close'. This phase handles callbacks for closing connections, such assocket.on('close', ...)
Thou should beware of nextTick
When you call nextTick all callbacks passed to it will execute before the loop resumes. Recursive calls to nextTick can “starve” the I/O, preventing the event loop from returning to the I/O phase.
P.S: Mentally reverse nextTick and setImmediate. Despite the name, nextTick is the more “immediate” of the two. nextTick fires before the next phase, whereas setImmediate runs on the next check phase of the loop.