JavaScript Event Loop¶
Khái niệm cơ bản¶
Event Loop là cơ chế cốt lõi cho phép JavaScript (single-threaded) xử lý các tác vụ bất đồng bộ mà không bị blocking. Đây là "trái tim" của JavaScript runtime, quản lý việc thực thi code, xử lý events, và callbacks.
Tại sao cần Event Loop?¶
- JavaScript là single-threaded: Chỉ có một thread chính để thực thi code
- Tránh blocking UI: Không làm đơ giao diện khi có tác vụ nặng
- Xử lý bất đồng bộ: Cho phép thực hiện nhiều tác vụ "cùng lúc"
- Responsive applications: Ứng dụng phản hồi mượt mà
- Ví dụ trực quan: Event Loop
Kiến trúc JavaScript Runtime¶
┌─────────────────────────────────────┐
│ Call Stack │ ← Ngăn xếp thực thi (LIFO)
│ function frames │ Thực thi code đồng bộ
└─────────────────────────────────────┘
│
▼ Event Loop
┌─────────────────────────────────────┐
│ Web APIs │ ← Browser/Node.js APIs
│ setTimeout, DOM events, fetch │ Xử lý bất đồng bộ
└─────────────────────────────────────┘
│
▼ Callbacks
┌─────────────────────────────────────┐
│ Microtask Queue │ ← Độ ưu tiên CAO
│ Promise.then, queueMicrotask │ Process callbacks
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Callback Queue │ ← Độ ưu tiên THẤP HỐN
│ (Task Queue/Macrotask Queue) │ setTimeout, DOM events
└─────────────────────────────────────┘
Các thành phần chính¶
1. Call Stack (Ngăn xếp gọi hàm)¶
Call Stack là nơi JavaScript thực thi code đồng bộ theo nguyên tắc LIFO.
function first() {
console.log('First function start');
second();
console.log('First function end');
}
function second() {
console.log('Second function start');
third();
console.log('Second function end');
}
function third() {
console.log('Third function');
}
first();
/* Call Stack execution:
┌─────────────┐
│ third() │ ← Executing
├─────────────┤
│ second() │
├─────────────┤
│ first() │
├─────────────┤
│ global │
└─────────────┘
Output:
First function start
Second function start
Third function
Second function end
First function end
*/
2. Web APIs (Browser APIs)¶
Web APIs là các API được cung cấp bởi browser hoặc Node.js runtime:
- Timers:
setTimeout,setInterval - DOM Events: click, scroll, resize
- HTTP Requests:
fetch,XMLHttpRequest - Storage: localStorage, sessionStorage
- Others:
requestAnimationFrame,IntersectionObserver
// Web API được gọi nhưng không block Call Stack
setTimeout(() => {
console.log('Timer callback');
}, 1000);
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
document.addEventListener('click', () => {
console.log('Click handler');
});
console.log('Synchronous code continues...');
// Output ngay lập tức: "Synchronous code continues..."
// Callbacks sẽ chạy sau khi timer/request hoàn thành
3. Callback Queue (Task Queue / Macrotask Queue)¶
Callback Queue chứa các callbacks từ Web APIs, hoạt động theo FIFO:
console.log('Start');
// Callback sẽ vào Callback Queue
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);
setTimeout(() => console.log('Timeout 3'), 0);
console.log('End');
/*
Call Stack: Start → End
Callback Queue: [Timeout 1] → [Timeout 2] → [Timeout 3]
Output:
Start
End
Timeout 1
Timeout 2
Timeout 3
*/
4. Microtask Queue¶
Microtask Queue có độ ưu tiên cao hơn Callback Queue:
console.log('Start');
// Macrotask
setTimeout(() => console.log('Timeout'), 0);
// Microtasks
Promise.resolve().then(() => console.log('Promise 1'));
Promise.resolve().then(() => console.log('Promise 2'));
console.log('End');
/*
Output:
Start
End
Promise 1 ← Microtasks được ưu tiên
Promise 2 ← Microtasks được ưu tiên
Timeout ← Macrotask chạy sau
*/
Quy trình hoạt động của Event Loop¶
Event Loop hoạt động theo chu kỳ với các bước sau:
Bước 1: Thực thi Call Stack¶
console.log('1'); // ← Thực thi ngay
console.log('2'); // ← Thực thi ngay
Bước 2: Kiểm tra Microtask Queue¶
Promise.resolve().then(() => console.log('Microtask')); // ← Ưu tiên cao
Bước 3: Kiểm tra Callback Queue¶
setTimeout(() => console.log('Macrotask'), 0); // ← Ưu tiên thấp hơn
Bước 4: Render (nếu cần)¶
Browser có thể render UI updates.
Bước 5: Lặp lại¶
Event Loop tiếp tục chu kỳ.
Ví dụ chi tiết¶
Ví dụ 1: Cơ bản¶
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
/*
Step-by-step execution:
1. console.log('1') → Call Stack → Output: "1"
2. setTimeout() → Web API → Callback vào Callback Queue sau 0ms
3. Promise.resolve().then() → Microtask Queue
4. console.log('4') → Call Stack → Output: "4"
5. Call Stack empty → Check Microtask Queue → Output: "3"
6. Microtask Queue empty → Check Callback Queue → Output: "2"
Final Output: 1, 4, 3, 2
*/
Ví dụ 2: Phức tạp hơn¶
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise inside timeout'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
return Promise.resolve();
})
.then(() => {
console.log('Promise 2');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
/*
Execution trace:
Call Stack: Start → End
Microtask Queue: [Promise 1] → [Promise 2]
Callback Queue: [Timeout 1] → [Timeout 2]
Output sequence:
Start
End
Promise 1
Promise 2
Timeout 1
Promise inside timeout ← New microtask created during timeout
Timeout 2
*/
Ví dụ 3: Nested Promises và Timeouts¶
console.log('Script start');
setTimeout(() => console.log('setTimeout 1'), 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('setTimeout 2'), 0);
})
.then(() => {
console.log('Promise 2');
});
setTimeout(() => console.log('setTimeout 3'), 0);
console.log('Script end');
/*
Output:
Script start
Script end
Promise 1
Promise 2
setTimeout 1
setTimeout 3
setTimeout 2
*/
Microtasks vs Macrotasks¶
Microtasks (Độ ưu tiên cao)¶
- Promise.then/catch/finally
- queueMicrotask()
- MutationObserver (trong browser)
- process.nextTick() (trong Node.js)
Macrotasks (Độ ưu tiên thấp hơn)¶
- setTimeout/setInterval
- setImmediate (Node.js)
- I/O operations
- UI events
- requestAnimationFrame
So sánh:¶
// Microtask
Promise.resolve().then(() => console.log('Microtask'));
queueMicrotask(() => console.log('queueMicrotask'));
// Macrotask
setTimeout(() => console.log('Macrotask'), 0);
console.log('Sync');
/*
Output:
Sync
Microtask
queueMicrotask
Macrotask
*/
Ứng dụng thực tế¶
Vấn đề: Blocking Code¶
// SAI - Blocking main thread
function heavyComputation() {
console.log('Starting heavy computation...');
// Simulation of heavy work - BLOCKS UI
for (let i = 0; i < 10000000000; i++) {
// Heavy computation that blocks the thread
Math.random();
}
console.log('Heavy computation done!');
}
// This will freeze the browser for several seconds
heavyComputation();
console.log('This message will be delayed');
Giải pháp: Non-blocking với Event Loop¶
// ĐÚNG - Non-blocking approach
function heavyComputationAsync(callback) {
console.log('Starting async heavy computation...');
let i = 0;
const total = 10000000;
const chunkSize = 100000;
function processChunk() {
let chunkEnd = Math.min(i + chunkSize, total);
// Process a small chunk
while (i < chunkEnd) {
Math.random();
i++;
}
// Update progress
console.log(`Progress: ${((i / total) * 100).toFixed(1)}%`);
if (i < total) {
// Schedule next chunk, giving browser time to breathe
setTimeout(processChunk, 0);
} else {
console.log('Heavy computation done!');
if (callback) callback();
}
}
// Start processing
processChunk();
}
// Usage
heavyComputationAsync(() => {
console.log('Computation finished callback');
});
console.log('This message appears immediately');
Debouncing với Event Loop¶
function debounce(func, delay) {
let timeoutId;
return function (...args) {
// Clear previous timeout
clearTimeout(timeoutId);
// Set new timeout
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Search input
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// Actual search API call here
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Throttling với requestAnimationFrame¶
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Smooth scroll handling
const throttledScrollHandler = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Update scroll-based animations
}, 16); // ~60fps
window.addEventListener('scroll', throttledScrollHandler);
Performance Best Practices¶
1. Tránh Long-running Tasks¶
// Bad - Long running task
function badDataProcessing(data) {
for (let item of data) {
// Complex processing
heavyOperation(item);
}
}
// Good - Break into chunks
async function goodDataProcessing(data) {
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
chunk.forEach(item => {
heavyOperation(item);
});
// Yield control back to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
2. Sử dụng Web Workers cho CPU-intensive tasks¶
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ command: 'start', data: largeDataSet });
worker.onmessage = function(e) {
if (e.data.type === 'progress') {
console.log('Progress:', e.data.progress);
} else if (e.data.type === 'complete') {
console.log('Result:', e.data.result);
}
};
// worker.js
self.onmessage = function(e) {
if (e.data.command === 'start') {
const result = processLargeData(e.data.data);
self.postMessage({ type: 'complete', result: result });
}
};
function processLargeData(data) {
// Heavy computation without blocking main thread
return data.map(item => complexCalculation(item));
}
3. Proper Promise Handling¶
// Bad - Creating unnecessary microtasks
async function badAsyncFunction() {
const result1 = await Promise.resolve(1);
const result2 = await Promise.resolve(2);
const result3 = await Promise.resolve(3);
return result1 + result2 + result3;
}
// Good - Parallel execution
async function goodAsyncFunction() {
const [result1, result2, result3] = await Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]);
return result1 + result2 + result3;
}
Common Pitfalls và Solutions¶
1. Mixing Sync và Async Code¶
// Problem
function mixedFunction() {
console.log('Start');
setTimeout(() => {
console.log('Async operation');
}, 0);
return 'Sync result'; // Returns before async operation
}
// Solution
async function asyncFunction() {
console.log('Start');
await new Promise(resolve => {
setTimeout(() => {
console.log('Async operation');
resolve();
}, 0);
});
return 'Result after async operation';
}
2. Event Loop Starvation¶
// Problem - Infinite microtasks
function starveEventLoop() {
Promise.resolve().then(() => {
console.log('Microtask');
starveEventLoop(); // Creates infinite loop of microtasks
});
}
// Solution - Use setTimeout to break the loop
function fixedFunction() {
Promise.resolve().then(() => {
console.log('Microtask');
setTimeout(() => fixedFunction(), 0); // Gives other tasks a chance
});
}
Debugging Event Loop¶
1. Visualizing Call Stack¶
function traceStack() {
console.trace('Current call stack');
}
function a() {
b();
}
function b() {
c();
}
function c() {
traceStack();
}
a(); // Shows full call stack
2. Performance Monitoring¶
// Measure task timing
function measureTask(taskName, task) {
const start = performance.now();
Promise.resolve().then(() => {
task();
const end = performance.now();
console.log(`${taskName} took ${end - start}ms`);
});
}
measureTask('Heavy calculation', () => {
for (let i = 0; i < 1000000; i++) {
Math.random();
}
});
3. Event Loop Monitoring¶
// Detect long tasks
let lastTime = performance.now();
function checkEventLoop() {
const currentTime = performance.now();
const delay = currentTime - lastTime;
if (delay > 50) { // Task took longer than 50ms
console.warn(`Long task detected: ${delay}ms`);
}
lastTime = currentTime;
setTimeout(checkEventLoop, 0);
}
checkEventLoop();
Kết luận¶
Event Loop là backbone của JavaScript asynchronous programming:
Key Takeaways:¶
- Single-threaded nhưng non-blocking nhờ Event Loop
- Microtasks luôn có độ ưu tiên cao hơn Macrotasks
- Call Stack phải empty trước khi Event Loop xử lý queues
- Performance phụ thuộc vào việc quản lý tasks đúng cách
Best Practices:¶
- Break long tasks thành chunks nhỏ
- Use Web Workers cho CPU-intensive operations
- Understand Promise behavior và microtask queue
- Avoid blocking main thread
- Monitor performance và detect long tasks
Nhớ rằng: "Understanding Event Loop is understanding JavaScript" - đây là chìa khóa để viết JavaScript hiệu quả và responsive!