Event Loop là gì?
Event Loop là cơ chế cốt lõi cho phép JavaScript xử lý các tác vụ bất đồng bộ mặc dù là single-threaded. Hiểu Event Loop giúp bạn nắm rõ cách JavaScript hoạt động “dưới hood”.
JavaScript là Single-Threaded
JavaScript chỉ có một thread duy nhất để thực thi code. Vậy làm sao nó có thể xử lý nhiều tác vụ cùng lúc? Đó là nhờ Event Loop!
console.log('1. Đầu tiên');
setTimeout(() => {
console.log('2. Timeout');
}, 0);
console.log('3. Cuối cùng');
// Output:
// 1. Đầu tiên
// 3. Cuối cùng
// 2. Timeout (tại sao lại sau?)
Các thành phần của Event Loop
1. Call Stack (Ngăn xếp gọi hàm)
Nơi lưu trữ các hàm đang được thực thi:
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4);
// Call Stack:
// 1. printSquare(4)
// 2. square(4)
// 3. multiply(4, 4)
// 4. console.log(16)
2. Web APIs
Browser cung cấp các API để xử lý tác vụ bất đồng bộ:
setTimeout()/setInterval()- DOM Events
fetch()/ AJAXPromise
3. Callback Queue (Task Queue)
Hàng đợi lưu các callback chờ thực thi:
setTimeout(() => console.log('Callback 1'), 0);
setTimeout(() => console.log('Callback 2'), 0);
// Callback Queue: [callback1, callback2]
4. Microtask Queue
Hàng đợi ưu tiên cao hơn cho:
- Promise callbacks (
.then(),.catch()) MutationObserverqueueMicrotask()
Cách Event Loop hoạt động
Quy trình
- Thực thi code đồng bộ trong Call Stack
- Khi Call Stack rỗng:
- Kiểm tra Microtask Queue → thực thi hết
- Kiểm tra Callback Queue → lấy 1 callback thực thi
- Lặp lại
Ví dụ chi tiết
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout
Giải thích:
console.log('Start')→ thực thi ngaysetTimeout()→ gửi callback vào Callback QueuePromise→.then()vào Microtask Queueconsole.log('End')→ thực thi ngay- Call Stack rỗng → kiểm tra Microtask Queue:
- Thực thi Promise 1
- Thực thi Promise 2
- Microtask Queue rỗng → kiểm tra Callback Queue:
- Thực thi Timeout
Macrotask vs Microtask
Macrotasks (Tasks)
setTimeoutsetIntervalsetImmediate(Node.js)- I/O operations
Microtasks
Promise.then/catch/finallyasync/awaitqueueMicrotask()MutationObserver
Quy tắc: Microtasks luôn được ưu tiên thực thi trước Macrotasks!
Ví dụ phức tạp
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
})
.then(() => console.log('5'));
console.log('6');
// Output: 1, 6, 3, 5, 2, 4
Phân tích:
1(đồng bộ)6(đồng bộ)3(microtask - Promise)5(microtask - Promise)2(macrotask - setTimeout)4(macrotask - setTimeout từ trong Promise)
Blocking Event Loop
Code đồng bộ chặn Event Loop
console.log('Start');
// Block event loop trong 5 giây!
const start = Date.now();
while (Date.now() - start < 5000) {
// Không làm gì
}
console.log('End');
// UI bị đóng băng trong 5 giây!
❌ Vấn đề
// Click button sẽ không hoạt động ngay lập tức
button.addEventListener('click', () => {
console.log('Clicked!');
});
// Chặn event loop
let count = 0;
while (count < 1000000000) {
count++;
}
✅ Giải pháp: Break thành chunks
function processLargeArray(array, chunkSize = 1000) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (; index < end; index++) {
// Process item
processItem(array[index]);
}
if (index < array.length) {
setTimeout(processChunk, 0); // Cho Event Loop breathe
}
}
processChunk();
}
Ví dụ thực tế
1. Animation smooth
function animate() {
// Update animation
updatePosition();
// Không block event loop
requestAnimationFrame(animate);
}
animate();
2. Debouncing với Event Loop
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Sử dụng
const handleSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
3. Load data không block UI
async function loadUserData(userId) {
// Show loading
showSpinner();
try {
// Không block UI vì await release event loop
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const orders = await fetch(`/api/orders/${userId}`).then(r => r.json());
displayUserData(user, orders);
} finally {
hideSpinner();
}
}
4. Priority Queue với queueMicrotask
console.log('1');
queueMicrotask(() => {
console.log('2 - High priority');
});
setTimeout(() => {
console.log('3 - Low priority');
}, 0);
Promise.resolve().then(() => {
console.log('4 - High priority');
});
console.log('5');
// Output: 1, 5, 2, 4, 3
Async/Await và Event Loop
async function example() {
console.log('1');
await Promise.resolve();
console.log('2'); // Microtask
await Promise.resolve();
console.log('3'); // Microtask
}
console.log('Start');
example();
console.log('End');
// Output: Start, 1, End, 2, 3
Giải thích: Mỗi await tạo một microtask mới.
setImmediate vs setTimeout (Node.js)
// Node.js
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Output không xác định!
// Có thể là: timeout, immediate HOẶC immediate, timeout
Trong I/O cycle:
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Output: immediate, timeout (xác định!)
Best Practices
1. Tránh blocking code
// ❌ BAD
function processData(largeArray) {
largeArray.forEach(item => {
// Heavy processing
});
}
// ✅ GOOD
async function processData(largeArray) {
for (const item of largeArray) {
await processItem(item);
// Cho event loop breathe sau mỗi item
}
}
2. Hiểu thứ tự ưu tiên
// Microtask > Macrotask
Promise.resolve().then(() => {
// Chạy trước
});
setTimeout(() => {
// Chạy sau
}, 0);
3. Sử dụng requestAnimationFrame cho animation
// ✅ Smooth animation
function animate() {
updateElement();
requestAnimationFrame(animate);
}
// ❌ Có thể lag
function animate() {
updateElement();
setTimeout(animate, 16); // 60fps
}
4. Break heavy tasks
function* heavyTask() {
for (let i = 0; i < 1000000; i++) {
yield processItem(i);
}
}
async function runTask() {
const task = heavyTask();
for (const item of task) {
if (shouldPause()) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
Kết luận
Event Loop là trái tim của JavaScript:
- Cho phép xử lý bất đồng bộ trên single thread
- Microtasks có priority cao hơn Macrotasks
- Hiểu Event Loop giúp viết code performance tốt
- Tránh blocking Event Loop để UI mượt mà
Nắm vững Event Loop là bước quan trọng để trở thành JavaScript developer chuyên nghiệp!