🔄 Bài 1: Event Loop Là Gì?
🎯 Sau bài học này, bạn sẽ:
- Hiểu JavaScript là single-threaded và tại sao cần Event Loop
- Biết Call Stack, Web APIs, Callback Queue hoạt động ra sao
- Phân biệt Microtask (Promise) và Macrotask (setTimeout)
- Hiểu Event Loop trong Node.js khác Browser như thế nào
- Nắm nền tảng để hiểu Message Queue ở các bài sau
1. JavaScript Là Single-Threaded
JavaScript chỉ có một thread duy nhất để thực thi code. Điều này có nghĩa là JavaScript chỉ có thể làm một việc tại một thời điểm. Vậy làm sao nó xử lý được nhiều tác vụ cùng lúc như fetch API, setTimeout, I/O?
Câu trả lời chính là Event Loop — cơ chế giúp JavaScript xử lý các tác vụ bất đồng bộ mà không cần multi-threading.
JavaScript được thiết kế ban đầu để chạy trên browser, thao tác DOM. Nếu multi-threaded, 2 thread cùng sửa 1 DOM element sẽ gây conflict. Single-threaded đơn giản hơn và an toàn hơn.
2. Các Thành Phần Của Event Loop
📦 Call Stack (Ngăn xếp lệnh)
Call Stack là nơi JavaScript thực thi các hàm theo thứ tự LIFO (Last In, First Out). Khi gọi một hàm, nó được đẩy vào stack. Khi hàm return, nó bị pop ra khỏi stack.
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 (từ dưới lên):
// 1. printSquare(4)
// 2. square(4)
// 3. multiply(4, 4) ← đang thực thi
// → multiply return 16
// → square return 16
// → console.log(16)
// → printSquare return
🌐 Web APIs / Node.js APIs
Khi gặp các tác vụ async như setTimeout, fetch, fs.readFile,
JavaScript ủy thác chúng cho runtime environment (Browser hoặc Node.js). Runtime xử lý
ở background bằng thread riêng.
📋 Callback Queue (Task Queue / Macrotask Queue)
Sau khi Web API xử lý xong, callback được đẩy vào Callback Queue. Event Loop sẽ kiểm tra: nếu Call Stack rỗng → lấy callback từ Queue đẩy vào Stack để thực thi.
⚡ Microtask Queue
Promise callbacks (.then(), .catch(), async/await) và
MutationObserver được đẩy vào Microtask Queue — queue này có ưu
tiên cao hơn Callback Queue.
┌─────────────────────────────────────────────────┐
│ Event Loop │
│ │
│ ┌──────────┐ ┌─────────────┐ │
│ │Call Stack │ │ Web APIs │ │
│ │ │ │ setTimeout │ │
│ │ func() │───▶│ fetch() │ │
│ │ │ │ I/O │ │
│ └──────────┘ └──────┬──────┘ │
│ ▲ │ │
│ │ ▼ │
│ │ ┌────────────────────┐ │
│ │ │ Microtask Queue │ ◀── Promise │
│ │ │ (ưu tiên cao) │ │
│ │ └────────┬───────────┘ │
│ │ │ │
│ │ ┌────────▼───────────┐ │
│ └────│ Callback Queue │ ◀── setTimeout │
│ │ (Macrotask) │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────┘
3. Microtask vs Macrotask
Đây là kiến thức cực kỳ quan trọng và thường xuất hiện trong phỏng vấn. Hiểu rõ sự khác biệt giúp bạn dự đoán chính xác thứ tự thực thi code.
| Loại | ⚡ Microtask | 📋 Macrotask |
|---|---|---|
| Ví dụ | Promise.then(), async/await, queueMicrotask(), MutationObserver | setTimeout, setInterval, setImmediate, I/O callbacks |
| Ưu tiên | Cao hơn (chạy trước) | Thấp hơn |
| Thực thi | Chạy TẤT CẢ microtask trước khi xử lý macrotask tiếp | Chạy 1 macrotask → rồi chạy hết microtask |
Ví dụ kinh điển
console.log('1. Start'); // Synchronous
setTimeout(() => {
console.log('6. setTimeout (Macrotask)');
}, 0);
Promise.resolve()
.then(() => {
console.log('3. Promise 1 (Microtask)');
})
.then(() => {
console.log('4. Promise 2 (Microtask)');
});
queueMicrotask(() => {
console.log('5. queueMicrotask (Microtask)');
});
console.log('2. End'); // Synchronous
// Output:
// 1. Start
// 2. End
// 3. Promise 1 (Microtask)
// 4. Promise 2 (Microtask)
// 5. queueMicrotask (Microtask)
// 6. setTimeout (Macrotask)
1. Code đồng bộ (synchronous) chạy trước hết
2. Microtask Queue chạy hết (tất cả) sau mỗi task
3. Macrotask Queue mỗi lần chỉ lấy 1 task
4. Lặp lại: 1 Macrotask → tất cả Microtask → 1 Macrotask → ...
4. Ví Dụ Nâng Cao: async/await
async function fetchData() {
console.log('2. Trong async function (sync phần trước await)');
const data = await Promise.resolve('Hello');
// ↑ Từ đây trở đi = microtask (giống .then())
console.log('5. Sau await:', data);
}
console.log('1. Start');
fetchData();
console.log('3. Sau gọi fetchData');
Promise.resolve().then(() => {
console.log('4. Promise.then (Microtask)');
});
// Output:
// 1. Start
// 2. Trong async function (sync phần trước await)
// 3. Sau gọi fetchData
// 4. Promise.then (Microtask)
// 5. Sau await: Hello
Code trước
await chạy đồng bộ. Code sau
await được chuyển thành microtask (tương đương .then()).
5. Event Loop Trong Node.js
Node.js sử dụng thư viện libuv để triển khai Event Loop, với các phase riêng biệt:
┌───────────────────────────┐
┌─▶│ Timers │ ← setTimeout, setInterval
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ Pending Callbacks │ ← I/O callbacks
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ Idle, Prepare │ ← Internal use
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ Poll │ ← I/O events (main phase)
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ Check │ ← setImmediate
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
└──│ Close Callbacks │ ← socket.on('close')
└───────────────────────────┘
* Microtask (Promise, process.nextTick)
chạy GIỮA MỖI phase!
// Node.js specific: process.nextTick vs Promise
setImmediate(() => {
console.log('4. setImmediate (Check phase)');
});
setTimeout(() => {
console.log('3. setTimeout (Timer phase)');
}, 0);
Promise.resolve().then(() => {
console.log('2. Promise (Microtask)');
});
process.nextTick(() => {
console.log('1. nextTick (Microtask - ưu tiên nhất)');
});
// Output:
// 1. nextTick (Microtask - ưu tiên nhất)
// 2. Promise (Microtask)
// 3. setTimeout (Timer phase)
// 4. setImmediate (Check phase)
Trong Node.js,
process.nextTick có ưu tiên cao hơn Promise. Cả hai đều là
microtask, nhưng nextTick luôn chạy trước.
6. Event Loop Liên Quan Gì Đến Message Queue?
Event Loop là nền tảng để hiểu Message Queue vì:
- Cùng mô hình: Message Queue hoạt động giống Callback Queue — message được đẩy vào hàng đợi và xử lý lần lượt
- Async processing: Consumer nhận message bất đồng bộ, giống cách Event Loop xử lý callback
- Non-blocking: Producer gửi message mà không cần chờ Consumer xử lý xong
- FIFO: Message Queue mặc định xử lý First-In-First-Out, giống Callback Queue
Event Loop trong JS: Message Queue trong hệ thống:
┌────────────┐ ┌────────────┐
│ Call Stack │ │ Service A │ (Producer)
└─────┬──────┘ └─────┬──────┘
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ Callback │ │ Message │
│ Queue │ │ Queue │ (RabbitMQ/Kafka)
└─────┬──────┘ └─────┬──────┘
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ Execute CB │ │ Service B │ (Consumer)
└────────────┘ └────────────┘
📝 Tóm Tắt Bài Học
- JavaScript là single-threaded, dùng Event Loop để xử lý async
- Call Stack: thực thi code đồng bộ theo LIFO
- Microtask (Promise, nextTick) ưu tiên cao hơn Macrotask (setTimeout)
- Node.js Event Loop có 6 phases, microtask chạy giữa mỗi phase
- Event Loop là nền tảng để hiểu cách Message Queue hoạt động