JavaScript Event Loop and Task Scheduling
eventLoop = {
taskQueues: {
// High priority tasks
input: [], // User input events (touches, clicks, etc.)
// Normal priority tasks
networking: [], // XHR, fetch, WebSockets
domEvents: [], // Most DOM events (except input)
mediaEvents: [], // Media events (video, audio)
// Low priority tasks
timers: [], // setTimeout, setInterval
parsing: [], // HTML parser
callbacks: [], // requestIdleCallback, etc.
},
microtaskQueue: [],
nextTask: function() {
// Chrome prioritizes task sources dynamically
// First check high priority queues
if (this.taskQueues.input.length > 0)
return this.taskQueues.input.shift();
// Then check normal priority queues
for (let q of [this.taskQueues.networking, this.taskQueues.domEvents, this.taskQueues.mediaEvents])
if (q.length > 0)
return q.shift();
// Finally check low priority queues
for (let q of [this.taskQueues.timers, this.taskQueues.parsing, this.taskQueues.callbacks])
if (q.length > 0)
return q.shift();
return null;
},
executeMicrotasks: function() {
// Process microtasks until queue is empty (including new ones)
while (this.microtaskQueue.length > 0) {
let task = this.microtaskQueue.shift();
task.execute();
}
},
needsRendering: function() {
return (
this.isVSyncTime() &&
(this.hasPendingVisualChanges() || this.hasHighPriorityInputEvents())
);
},
render: function() {
// Input events are handled with highest priority
this.dispatchPendingInputEvents();
// Update layout tree if needed
if (this.hasPendingStyleOrLayoutChanges()) {
this.recalculateStyles();
this.updateLayout();
}
// Run pre-paint steps
this.runAnimationFrameCallbacks();
this.updateMediaQueries();
this.runCSSAnimations();
this.runCSSTransitions();
// Observer callbacks
this.runResizeObservers();
this.runIntersectionObservers();
// Compositing and rendering
this.updateLayerTree();
this.paint();
this.composite();
},
runIdleCallbacks: function(deadline) {
while (this.hasIdleCallbacks() && deadline.timeRemaining() > 0) {
this.executeNextIdleCallback(deadline);
}
}
};
while (true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
eventLoop.executeMicrotasks();
}
if (eventLoop.needsRendering()) {
eventLoop.render();
}
// If there's idle time, run idle callbacks
if (!task && !eventLoop.needsRendering()) {
let deadline = eventLoop.createIdleDeadline();
eventLoop.runIdleCallbacks(deadline);
}
}
normally the task was ordered by FIFO
example 1
the first setTimeout task was poped out first, beacuse it pushed to task-queue first
setTimeout(function () {console.log(1)}, 6);
let i = 0;
while(i < 1000000000) {
i++;
}
setTimeout(function () {console.log(2)}, 4);
// print
// 1
// 2
Nodejs Event Loop
Nodejs event loop is similar to browser’s, but it has some differences
eventLoop = {
// Phase queues
timers: [], // setTimeout, setInterval callbacks
pendingCallbacks: [], // I/O callbacks deferred to the next iteration
idleHandlers: [], // setImmediate callbacks
poll: {
events: [], // I/O events ready to be processed
timeout: null // For blocking in poll phase
},
check: [], // setImmediate callbacks (alias for idleHandlers)
close: [], // close event callbacks
// Microtasks are handled differently in Node.js
nextTickQueue: [], // process.nextTick callbacks
promiseQueue: [], // Promise resolution callbacks
processMicrotasks: function() {
// First, process all nextTick callbacks
while (this.nextTickQueue.length > 0) {
let callback = this.nextTickQueue.shift();
callback();
}
// Then, process all promise callbacks
while (this.promiseQueue.length > 0) {
let callback = this.promiseQueue.shift();
callback();
}
},
runPhase: function(phase) {
let queue = this[phase];
if (Array.isArray(queue)) {
// Handle regular array queues
while (queue.length > 0) {
let callback = queue.shift();
callback();
// Process microtasks after each callback
this.processMicrotasks();
}
} else if (phase === 'poll') {
// Special handling for poll phase
this.processPollPhase();
}
},
processPollPhase: function() {
// Process any pending I/O events
while (this.poll.events.length > 0) {
let event = this.poll.events.shift();
event.callback();
// Process microtasks after each callback
this.processMicrotasks();
}
// If there are setImmediate callbacks or the poll queue is not empty
// after processing events, don't block
if (this.check.length > 0 || this.poll.events.length > 0) {
return;
}
// If there are timers scheduled, block until the next timer expires
if (this.timers.length > 0) {
this.poll.timeout = this.getNextTimerExpiry();
} else {
// Otherwise, potentially block indefinitely (or until I/O event)
this.poll.timeout = Infinity;
}
// (In reality, this is where libuv would block waiting for I/O events)
},
getNextTimerExpiry: function() {
// This would calculate when the next timer is due to expire
// Simplified for this example
return Date.now() + 1000;
}
};
// Main event loop
function startEventLoop() {
while (true) {
// Phase 1: Timers
eventLoop.runPhase('timers');
// Phase 2: Pending callbacks
eventLoop.runPhase('pendingCallbacks');
// Phase 3: Idle, prepare (internal only - not represented here)
// Phase 4: Poll
eventLoop.runPhase('poll');
// Phase 5: Check
eventLoop.runPhase('check');
// Phase 6: Close callbacks
eventLoop.runPhase('close');
// Check if we need to exit the event loop
if (this.shouldExit()) {
break;
}
}
}
function shouldExit() {
// In Node.js, the event loop exits when there are no more:
// - Active handles (timers, I/O, etc.)
// - Active requests (ongoing operations)
// - Pending callbacks in any queue
return (
eventLoop.timers.length === 0 &&
eventLoop.pendingCallbacks.length === 0 &&
eventLoop.idleHandlers.length === 0 &&
eventLoop.poll.events.length === 0 &&
eventLoop.check.length === 0 &&
eventLoop.close.length === 0 &&
eventLoop.nextTickQueue.length === 0 &&
eventLoop.promiseQueue.length === 0
);
}
startEventLoop();
example 2
import { Duplex } from 'node:stream';
const stream = new Duplex();
stream._read = () => {}
async function main() {
const promise = new Promise((resolve, reject) => {
stream.on("data", (chunk) => console.log('data'));
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve("done"));
});
console.log("Waiting...");
await promise;
console.log("Done");
}
main()
The output is:
Waiting...
There is not active handles, so the event loop will not block, and the event loop will exit.