[Advanced JS] Event Loop là gì? Khám phá cơ chế hoạt động của JavaScript

JavaScript, ngôn ngữ của thế giới web, có một bí mật đằng sau khả năng xử lý hàng loạt tác vụ mà không hề "tắc nghẽn": Event Loop. Đối với bất kỳ lập trình viên JavaScript nào, từ người mới bắt đầu đến chuyên gia, việc hiểu rõ Event Loop không chỉ là một lựa chọn, mà là một yêu cầu bắt buộc để viết mã hiệu quả, tối ưu và không bị chặn (non-blocking).

Vậy Event Loop là gì? Hãy tưởng tượng nó như một người điều phối giao thông thông minh trong một thành phố chỉ có một làn đường duy nhất. Người điều phối này đảm bảo rằng mọi "chiếc xe" (tác vụ) đều di chuyển một cách trật tự, không gây ùn tắc, kể cả khi có những chiếc xe cần dừng lại để xử lý việc riêng.

Event Loop là gì? Khám phá cơ chế hoạt động của JavaScript

Bài viết này sẽ giúp bạn tìm hiểu thật chuyên sâu về Event Loop, từ những khái niệm cơ bản nhất đến cách nó vận hành trong thực tế.

Tại sao lại cần Event Loop? Bí mật của "một làn đường" 🛣️

Để hiểu Event Loop, trước tiên chúng ta cần nắm vững một đặc tính cốt lõi của JavaScript: đơ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. Giống như một người chỉ có thể làm một việc tại một thời điểm.

JavaScript Single Thread

Vậy vấn đề là gì?

Hãy tưởng tượng bạn có một tác vụ tốn rất nhiều thời gian, ví dụ như tải một file lớn từ mạng về. Nếu JavaScript thực thi tác vụ này một cách tuần tự, toàn bộ trang web của bạn sẽ bị "đóng băng". Người dùng không thể nhấp vào nút, không thể cuộn trang, không thể làm bất cứ điều gì cho đến khi file được tải xong. Trải nghiệm người dùng sẽ trở thành một thảm họa.

Đây chính là lúc mô hình bất đồng bộ (asynchronous) và Event Loop tỏa sáng. JavaScript Engine (như V8 của Chrome) kết hợp với các môi trường chạy (như trình duyệt hoặc Node.js) để tạo ra một cơ chế xử lý thông minh, cho phép các tác vụ "dài hơi" được thực hiện trong nền mà không làm ảnh hưởng đến luồng chính.

Các thành phần chính trong "dàn nhạc" JavaScript 🎺

Để Event Loop hoạt động, nó cần sự phối hợp nhịp nhàng của nhiều thành phần. Hãy coi chúng như một dàn nhạc, mỗi nhạc công có một vai trò riêng.

1. Call Stack

Đây là "sân khấu" chính, nơi các hàm được thực thi. Nó hoạt động theo cơ chế LIFO (Last-In, First-Out) - vào sau, ra trước.

Call Stack

  • Khi một hàm được gọi, nó sẽ được đẩy (push) vào đỉnh của Stack.
  • Khi hàm đó thực thi xong và trả về kết quả, nó sẽ được lấy ra (pop) khỏi Stack.
function greet() {
  console.log('Hello!')
}

function sayHello() {
  greet()
}

sayHello() // Bắt đầu thực thi

Trong ví dụ trên, sayHello() được đẩy vào Stack, sau đó greet() được đẩy vào trên cùng. greet() thực thi xong, bị pop ra, rồi đến sayHello(). Call Stack cuối cùng sẽ trống.

2. Web APIs

Đây là nơi xử lý các tác vụ bất đồng bộ, những công việc "nặng nhọc" mà chúng ta không muốn nó chặn luồng chính. Các API này không phải là một phần của JavaScript Engine, mà được cung cấp bởi môi trường chạy (trình duyệt). Ví dụ điển hình:

  • setTimeout, setInterval
  • Thao tác DOM (ví dụ: addEventListener)
  • AJAX requests (fetch, XMLHttpRequest)

Khi Call Stack gặp một lệnh gọi đến Web API (ví dụ setTimeout), nó sẽ "ủy thác" công việc này cho trình duyệt và ngay lập tức chuyển sang tác vụ tiếp theo mà không cần chờ đợi.

3. Callback Queue (Task Queue)

Khi một tác vụ bất đồng bộ trong Web APIs hoàn thành (ví dụ, setTimeout hết thời gian), hàm callback tương ứng của nó (hàm được truyền vào setTimeout) sẽ không được thực thi ngay lập tức. Thay vào đó, nó sẽ được đẩy vào một hàng đợi gọi là Callback Queue.

Callback Queue

Hàng đợi này hoạt động theo cơ chế FIFO (First-In, First-Out) - vào trước, ra trước.

4. Microtask Queue

Đây là một hàng đợi đặc biệt, có độ ưu tiên cao hơn Callback Queue. Nó được dùng để chứa các callback của những tác vụ bất đồng bộ hiện đại hơn như Promise (hàm then, catch, finally) và MutationObserver.

Điểm mấu chốt: Event Loop sẽ luôn ưu tiên xử lý hết tất cả các tác vụ trong Microtask Queue trước khi ngó ngàng đến bất kỳ tác vụ nào trong Callback Queue.

Event Loop: "Người điều phối" không biết mệt mỏi 🤵🏻

Bây giờ, hãy kết nối tất cả các mảnh ghép lại. Event Loop có một nhiệm vụ duy nhất nhưng vô cùng quan trọng:

Liên tục kiểm tra xem Call Stack có trống không. Nếu trống, nó sẽ lấy tác vụ đầu tiên từ các hàng đợi (ưu tiên Microtask Queue trước) và đẩy vào Call Stack để thực thi.

Vòng lặp này diễn ra liên tục, đảm bảo rằng không có thời gian chết và các tác vụ được xử lý một cách hiệu quả.

Event Loop và Cơ chế hoạt động của JavaScript

Quy trình hoạt động hoàn chỉnh:

  1. Mã JavaScript bắt đầu chạy. Các hàm đồng bộ (synchronous) được đẩy vào Call Stack và thực thi ngay lập tức.
  2. Khi gặp một tác vụ bất đồng bộ (như setTimeout), nó được gửi đến Web APIs để xử lý. JavaScript không chờ đợi mà tiếp tục thực thi các lệnh tiếp theo trong Call Stack.
  3. Web API xử lý xong tác vụ (ví dụ: bộ đếm thời gian của setTimeout kết thúc), nó sẽ đẩy hàm callback tương ứng vào Callback Queue (hoặc Microtask Queue nếu là Promise).
  4. Event Loop liên tục theo dõi Call Stack.
  5. Ngay khi Call Stack trống (tức là tất cả các mã đồng bộ đã chạy xong), Event Loop sẽ kiểm tra Microtask Queue.
  6. Nếu có tác vụ trong Microtask Queue, Event Loop sẽ lấy tác vụ đầu tiên, đẩy vào Call Stack và thực thi. Nó sẽ lặp lại cho đến khi Microtask Queue trống hoàn toàn.
  7. Sau khi Microtask Queue đã trống, Event Loop mới kiểm tra đến Callback Queue. Nếu có tác vụ, nó sẽ lấy tác vụ đầu tiên và đẩy vào Call Stack.
  8. Quá trình này lặp đi lặp lại mãi mãi, tạo thành một "vòng lặp sự kiện".

Ví dụ kinh điển

Hãy xem đoạn mã sau và đoán thứ tự in ra console:

console.log('Bắt đầu') // 1

setTimeout(() => {
  console.log('Trong setTimeout - Callback Queue') // 4
}, 0)

Promise.resolve().then(() => {
  console.log('Trong Promise - Microtask Queue') // 3
})

console.log('Kết thúc') // 2

Giải thích từng bước:

  1. console.log("Bắt đầu") là đồng bộ, được đẩy vào Call Stack và thực thi ngay. Console in ra: Bắt đầu.
  2. setTimeout được gặp. Call Stack đẩy nó cho Web APIs xử lý. Timer được đặt là 0ms, nhưng hàm callback của nó vẫn phải chờ để được đưa vào Callback Queue.
  3. Promise.resolve().then(...) được gặp. Hàm .then() là bất đồng bộ. Callback của nó được đưa ngay vào Microtask Queue.
  4. console.log("Kết thúc") là đồng bộ, được đẩy vào Call Stack và thực thi. Console in ra: Kết thúc.
  5. Lúc này, Call Stack đã trống. Event Loop bắt đầu làm việc.
  6. Event Loop kiểm tra Microtask Queue trước. Nó thấy có một callback từ Promise. Callback này được đẩy vào Call Stack và thực thi. Console in ra: Trong Promise - Microtask Queue.
  7. Microtask Queue giờ đã trống. Event Loop chuyển sang kiểm tra Callback Queue. Nó thấy có callback từ setTimeout.
  8. Callback này được đẩy vào Call Stack và thực thi. Console in ra: Trong setTimeout - Callback Queue.

Kết quả cuối cùng:

Bắt đầu
Kết thúc
Trong Promise - Microtask Queue
Trong setTimeout - Callback Queue

Kết luận: Tại sao phải quan tâm tới Event Loop?

Hiểu về Event Loop mang lại những lợi ích to lớn:

  • Viết mã không chặn (Non-blocking code): Bạn có thể thực hiện các tác vụ tốn thời gian (gọi API, đọc file) mà không làm đóng băng giao diện người dùng.
  • Tối ưu hóa hiệu suất: Sắp xếp và ưu tiên các tác vụ một cách hợp lý để ứng dụng chạy mượt mà hơn.
  • Gỡ lỗi hiệu quả: Dễ dàng truy vết và hiểu được tại sao các đoạn mã bất đồng bộ lại chạy theo một thứ tự không như mong đợi.
  • Nắm vững các khái niệm nâng cao: Là nền tảng để hiểu sâu hơn về async/await, workers, và các kiến trúc ứng dụng phức tạp.

Event Loop chính là minh chứng cho sự tinh tế và mạnh mẽ trong thiết kế của JavaScript. Bằng cách làm chủ được nó, bạn không chỉ đang viết mã, mà bạn đang thực sự "đối thoại" với ngôn ngữ, điều khiển dòng chảy của ứng dụng một cách chuyên nghiệp và hiệu quả.

Bài viết liên quan

[Advanced JS] IIFE là gì? Ứng dụng nâng cao của IIFE trong JavaScript

Tìm hiểu về IIFE (Immediately Invoked Function Expression) và cách nó hoạt động trong JavaScript. Khám phá các ứng dụng thực tế của IIFE để tạo scope và bảo vệ biến.

[Advanced JS] Closure là gì? Ứng dụng nâng cao của Closure trong JavaScript

Closure là một khái niệm nâng cao nhưng cực kỳ hữu ích trong JavaScript. Bài viết này sẽ giúp bạn hiểu rõ về Closure thông qua các ví dụ trực quan, từ đó áp dụng hiệu quả vào dự án của mình.

[Advanced JS] Cấu trúc dữ liệu & Thuật toán: Ứng dụng thực tế với JavaScript

Ứng dụng Cấu trúc Dữ liệu và Thuật toán để tối ưu hiệu suất ứng dụng web. Nâng cao kỹ năng viết code sạch và tối ưu với JavaScript qua bài viết chuyên sâu này.

[JS Basics] Cách thiết lập môi trường chạy mã JavaScript

Bạn là người mới học lập trình và muốn bắt đầu với JavaScript? Bài viết này sẽ hướng dẫn bạn từng bước cách thiết lập môi trường chạy JavaScript trên máy tính một cách dễ dàng và nhanh chóng nhất.