Bạn đã bao giờ thắc mắc JavaScript xử lý đa nhiệm thế nào?
Nếu bạn là một lập trình viên JavaScript, chắc hẳn bạn đã quen thuộc với việc ngôn ngữ này là "đơn luồng" (single-threaded). Điều này có nghĩa là tại một thời điểm, JavaScript chỉ có thể thực hiện một tác vụ duy nhất. Nghe có vẻ hạn chế, đúng không? Vậy làm thế nào mà các tác vụ tốn thời gian như gọi API, đọc file, hay hẹn giờ (setTimeout) lại không "đóng băng" ứng dụng của chúng ta?
Câu trả lời nằm ở một cơ chế siêu việt mang tên Event Loop. Đây chính là "trái tim" giúp JavaScript, dù đơn luồng, vẫn có thể xử lý các tác vụ bất đồng bộ một cách mượt mà, mang lại trải nghiệm người dùng không bị gián đoạn.
Event Loop là gì?
Đơn giản mà nói, Event Loop là một cơ chế liên tục kiểm tra xem liệu Call Stack có trống không, và nếu có, nó sẽ đẩy các tác vụ từ Callback Queue (hay Task Queue) vào Call Stack để thực thi. Nghe có vẻ trừu tượng? Đừng lo, chúng ta sẽ "giải phẫu" nó từng phần một.
"Hệ Sinh Thái" Của Event Loop
Để hiểu Event Loop, chúng ta cần biết các thành phần chính "sống chung" với nó:
- Call Stack (Ngăn xếp lệnh gọi): Nơi lưu trữ các hàm đang được thực thi. Khi một hàm được gọi, nó được đẩy vào stack. Khi hàm kết thúc, nó được bật ra khỏi stack. JavaScript chỉ có thể thực thi các hàm trong Call Stack.
- Heap (Bộ nhớ Heap): Nơi lưu trữ các đối tượng và biến của ứng dụng.
- Web APIs (trong trình duyệt) / Node.js APIs (trong Node.js): Đây không phải là một phần của JavaScript engine mà là các môi trường mà JavaScript chạy trên đó cung cấp. Chúng bao gồm các hàm bất đồng bộ như
setTimeout(),fetch(), DOM events (click,scroll), đọc file, v.v. Khi bạn gọi một hàm bất đồng bộ, nó sẽ được chuyển giao cho Web APIs/Node.js APIs để xử lý. - Callback Queue (Task Queue / Message Queue): Nơi lưu trữ các hàm callback đã hoàn thành từ Web APIs/Node.js APIs và sẵn sàng được đẩy vào Call Stack. Ví dụ: callback của
setTimeout, handler của DOM event, callback củafs.readFile. - Microtask Queue: Một hàng đợi có độ ưu tiên cao hơn Callback Queue. Nó chứa các microtask như callback của
Promise.then(),Promise.catch(),Promise.finally(),queueMicrotask(), vàMutationObserver.
Event Loop Hoạt Động Như Thế Nào?
Hãy tưởng tượng bạn đang xếp hàng mua cà phê (Call Stack) và có một vài người bạn nhờ bạn mua hộ đồ ăn vặt (Web APIs/Node.js APIs). Sau khi mua được đồ ăn, họ sẽ đợi bạn ở một khu vực riêng (Callback Queue/Microtask Queue) để lấy đồ.
- JavaScript bắt đầu thực thi code từ trên xuống dưới, đẩy các hàm vào Call Stack.
- Khi gặp một hàm bất đồng bộ (ví dụ:
setTimeout(cb, 0)hoặcfetch(...)), JavaScript sẽ đẩy hàm đó (cùng với callback của nó) sang Web APIs/Node.js APIs để xử lý. Hàm bất đồng bộ này sẽ lập tức được bật ra khỏi Call Stack, cho phép JavaScript tiếp tục thực thi các tác vụ khác. - Khi Web APIs/Node.js APIs hoàn thành tác vụ (ví dụ: thời gian
setTimeoutkết thúc, dữ liệufetchđã về), hàm callback tương ứng sẽ được đẩy vào Callback Queue hoặc Microtask Queue. - Đây là lúc Event Loop vào cuộc! Nó liên tục kiểm tra:
- Trước tiên, Event Loop sẽ kiểm tra Microtask Queue. Nếu có bất kỳ microtask nào, nó sẽ đẩy TẤT CẢ microtask đó vào Call Stack để thực thi cho đến khi Microtask Queue trống rỗng.
- Chỉ khi Call Stack và Microtask Queue đều trống, Event Loop mới kiểm tra Callback Queue. Nếu có tác vụ trong Callback Queue, nó sẽ lấy một tác vụ (callback) đầu tiên và đẩy vào Call Stack để thực thi.
- Quá trình này lặp đi lặp lại không ngừng, giúp JavaScript xử lý các tác vụ bất đồng bộ mà không chặn luồng chính.
Ví dụ Minh Họa "Kinh Điển"
Hãy xem đoạn code này và thử đoán output:
console.log('Start');setTimeout(() => { console.log('Timeout callback');}, 0);Promise.resolve().then(() => { console.log('Promise callback');});console.log('End');Output sẽ là:
StartEndPromise callbackTimeout callbackTại sao 'Promise callback' lại xuất hiện trước 'Timeout callback' dù cả hai đều là bất đồng bộ và setTimeout có độ trễ 0ms? Đó chính là do sự khác biệt giữa Microtask (Promise callback) và Macrotask (setTimeout callback) và thứ tự ưu tiên của Event Loop.
Tại Sao Bạn Cần Quan Tâm Đến Event Loop?
Hiểu về Event Loop không chỉ giúp bạn trả lời phỏng vấn mà còn giúp bạn:
- Debug hiệu quả hơn: Khi ứng dụng có vấn đề về thứ tự thực thi các tác vụ bất đồng bộ, bạn sẽ biết cách "truy vết" vấn đề.
- Viết code tối ưu: Tránh các lỗi như "blocking main thread" (chặn luồng chính) gây giật lag UI.
- Nắm vững cơ chế bất đồng bộ: Là nền tảng để làm việc với
async/await, Promises, callbacks một cách tự tin.
Kết Luận
Event Loop là một trong những khái niệm quan trọng nhất trong JavaScript, là "người hùng thầm lặng" giúp ngôn ngữ này xử lý mọi thứ một cách phi đồng bộ mà vẫn mượt mà. Nắm vững Event Loop không chỉ là một kỹ năng, mà còn là chìa khóa để bạn trở thành một lập trình viên JavaScript thực thụ, có khả năng viết ra những ứng dụng mạnh mẽ và hiệu quả. Hãy nhớ rằng, trong thế giới JavaScript, không có gì là thực sự đồng bộ!