Single-threaded and event loop
The Nature of Single-Threadedness
JavaScript is a single-threaded language, meaning it can only execute one task at a time. This design stems from its original role as a browser scripting language, avoiding the complexities introduced by multithreading, such as race conditions and deadlocks. The single-threaded model simplifies development but also presents performance challenges.
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
// Output order: Start -> End -> Timeout
This example demonstrates the execution order in a single-threaded environment. Even with setTimeout
set to a delay of 0, the callback function will only execute after the current execution stack is cleared. This mechanism is a core manifestation of the event loop.
Call Stack and Task Queue
The call stack is the JavaScript engine's mechanism for tracking the order of function execution. When a function is called, it is pushed onto the call stack; once execution completes, it is popped off. Synchronous code is executed sequentially in the stack.
function first() {
console.log('First');
second();
}
function second() {
console.log('Second');
}
first();
// Call stack changes:
// [first] -> [first, second] -> [first] -> []
Asynchronous operations like setTimeout
or fetch
place callback functions in the task queue. Task queues are divided into two types:
- Macro task queue: Includes
setTimeout
,setInterval
, I/O, etc. - Micro task queue: Includes
Promise.then
,MutationObserver
, etc.
The Workflow of the Event Loop
The event loop continuously checks the call stack and task queues, processing tasks in a specific order:
- Execute all synchronous code in the current call stack.
- Check the micro task queue and execute all micro tasks.
- Execute one macro task.
- Repeat the process.
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Script end');
/*
Output order:
Script start
Script end
Promise 1
Promise 2
setTimeout
*/
This example clearly illustrates the characteristic where micro tasks take precedence over macro tasks. Promise callbacks, being micro tasks, execute immediately after the current stack is cleared, while setTimeout
callbacks, as macro tasks, are processed in the next event loop iteration.
Blocking vs. Non-Blocking I/O
Although JavaScript is single-threaded, it achieves efficient concurrency through non-blocking I/O and the event loop. Web APIs in the browser environment (e.g., fetch
, setTimeout
) and I/O operations in Node.js are asynchronous and non-blocking.
// Simulate a time-consuming synchronous operation
function syncOperation() {
const start = Date.now();
while (Date.now() - start < 3000) {}
console.log('Sync operation done');
}
// Asynchronous non-blocking operation
function asyncOperation() {
setTimeout(() => {
console.log('Async operation done');
}, 3000);
}
console.log('Start');
syncOperation(); // Blocks for 3 seconds
asyncOperation(); // Does not block
console.log('End');
Priority of Macro Tasks vs. Micro Tasks
Understanding the execution order of macro and micro tasks is crucial for writing efficient code. Micro tasks execute immediately after the current macro task ends, while new macro tasks wait for the next event loop iteration.
// Example 1: Nested tasks
setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro 1'));
}, 0);
setTimeout(() => {
console.log('macro 2');
Promise.resolve().then(() => console.log('micro 2'));
}, 0);
/*
Possible output:
macro 1
micro 1
macro 2
micro 2
*/
// Example 2: Generating new micro tasks within a micro task
Promise.resolve().then(() => {
console.log('micro 1');
Promise.resolve().then(() => console.log('micro 2'));
});
/*
Output:
micro 1
micro 2
*/
Performance Considerations in Practice
Long-running synchronous code can block the event loop, causing the page to become unresponsive. Web Workers can be used to offload compute-intensive tasks.
// Main thread
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
console.log('Result from worker:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = processLargeData(e.data);
self.postMessage(result);
};
For UI updates, requestAnimationFrame
ensures smooth animations:
function animate() {
// Update UI
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Evolution of Asynchronous Programming Patterns
From callback hell to modern asynchronous patterns, JavaScript's approach to handling asynchrony has evolved:
- Callback functions:
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
console.log(data);
});
- Promise chains:
fetch('/api/data')
.then(response => response.json())
.then(data => processData(data))
.catch(error => handleError(error));
- Async/Await:
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return processData(data);
} catch (error) {
handleError(error);
}
}
Differences in Node.js Event Loop
The Node.js event loop differs slightly from the browser's, with additional phases:
- Timers: Execute
setTimeout
andsetInterval
callbacks. - Pending callbacks: Execute system operation callbacks.
- Idle, prepare: Internal use.
- Poll: Retrieve new I/O events.
- Check: Execute
setImmediate
callbacks. - Close callbacks: Execute close event callbacks.
// Node.js-specific example
setImmediate(() => {
console.log('immediate');
});
setTimeout(() => {
console.log('timeout');
}, 0);
// Output order may vary depending on context
Common Pitfalls and Misconceptions
- Misunderstanding
setTimeout(fn, 0)
as immediate execution:
// It actually executes as soon as possible but still waits for the current stack to clear
setTimeout(() => console.log('timeout'), 0);
heavyCalculation(); // This delays the timeout output
- Closure issues in loops:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0);
}
// Outputs five 5s instead of 0-4
// Solution: Use let or additional closures
- Unhandled Promise rejections:
function riskyOperation() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) reject('Error');
else resolve('Success');
});
}
// Always add a catch handler
riskyOperation().catch(console.error);
Debugging and Performance Analysis
Chrome DevTools offers powerful event loop debugging capabilities:
- The Performance panel records the entire execution process.
- The Console shows unhandled Promise rejections.
- The
queueMicrotask
API directly adds micro tasks.
// Measure code execution time
console.time('operation');
expensiveOperation();
console.timeEnd('operation');
// Track micro tasks
queueMicrotask(() => {
console.log('Microtask executed');
});
Advanced Use Cases
- Implementing a custom scheduler:
class TaskScheduler {
constructor() {
this.queue = [];
this.isProcessing = false;
}
addTask(task) {
this.queue.push(task);
if (!this.isProcessing) this.processQueue();
}
processQueue() {
this.isProcessing = true;
queueMicrotask(() => {
const task = this.queue.shift();
if (task) task();
if (this.queue.length) this.processQueue();
else this.isProcessing = false;
});
}
}
- Optimizing batched DOM updates:
function batchDOMUpdates(updates) {
Promise.resolve().then(() => {
document.body.style.display = 'none';
updates.forEach(update => update());
document.body.style.display = '';
});
}
- Implementing React-like scheduling:
const taskQueue = [];
let isPerformingWork = false;
function scheduleTask(task) {
taskQueue.push(task);
if (!isPerformingWork) {
isPerformingWork = true;
requestIdleCallback(performWork);
}
}
function performWork(deadline) {
while (deadline.timeRemaining() > 0 && taskQueue.length) {
const task = taskQueue.shift();
task();
}
if (taskQueue.length) {
requestIdleCallback(performWork);
} else {
isPerformingWork = false;
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn