Quay lại danh sách
JavaScript
2024-03-25
25 phút đọc

Giải mã Event Loop: Tại sao JavaScript nhanh dù chỉ có đơn luồng?

Giải mã Event Loop trong JavaScript

JavaScript nổi tiếng là ngôn ngữ đơn luồng (Single-threaded). Vậy tại sao nó lại được dùng để xây dựng các hệ thống backend cực nhanh như Node.js? Câu trả lời nằm ở Event Loop.

Thay vì tạo hàng nghìn thread như trong nhiều ngôn ngữ khác, JavaScript tận dụng cơ chế non-blocking I/O và hàng đợi sự kiện để mô phỏng khả năng xử lý đồng thời trên một luồng duy nhất.

1. Cấu trúc bộ nhớ

  • Call Stack: Nơi lưu trữ các hàm đang thực thi. Theo cơ chế LIFO (Last In First Out).
  • Web APIs: Được trình duyệt (hoặc C++ APIs trong Node) cung cấp để xử lý các tác vụ như setTimeout, fetch, DOM events.
  • Task Queue (Macrotasks): Chứa các callback từ setTimeout, setInterval.
  • Microtask Queue: Ưu tiên cao hơn Macrotasks. Chứa các callback từ Promise.then, process.nextTick.

2. Quy trình của Event Loop

  1. Kiểm tra Call Stack. Nếu trống, chuyển sang bước 2.
  2. Kiểm tra Microtask Queue. Thực thi tất cả các task có trong hàng đợi này.
  3. Kiểm tra Macrotask Queue. Thực thi một task, sau đó quay lại bước 1.

Điểm quan trọng: Microtask luôn được ưu tiên hơn Macrotask. Điều này giải thích nhiều “câu đố” liên quan đến thứ tự log khi kết hợp Promise và setTimeout.

3. Ví dụ kinh điển

Bạn đoán xem kết quả sẽ là gì?

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

Kết quả là: 1, 4, 3, 2. Tại sao? Vì Promise vào Microtask Queue, còn setTimeout vào Macrotask Queue.

4. Ứng dụng thực tế trong UI và backend

Khi làm việc với các tác vụ nặng (như xử lý ảnh), nếu bạn chạy nó trực tiếp trên main thread, giao diện sẽ bị "đơ". Hiểu Event Loop giúp bạn biết cách tách các tác vụ này ra (sử dụng Web Workers hoặc chia nhỏ tác vụ với setImmediate).

Trong frontend, bạn có thể chia nhỏ một vòng lặp lớn thành nhiều batch nhỏ và dùng setTimeout(..., 0) hoặc requestIdleCallback để nhường quyền chạy cho browser giữa các batch, tránh chặn render.

Trong Node.js, việc hiểu rõ Event Loop giúp bạn:

  • Không block thread bằng các hàm sync như fs.readFileSync trong luồng xử lý request.
  • Thiết kế API không bị “callback hell” bằng cách dùng Promise/async-await nhưng vẫn tôn trọng bản chất async.

5. Một ví dụ phức tạp hơn

Hãy xem đoạn code sau và tự suy nghĩ thứ tự log:

console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('C');
    return Promise.resolve();
  })
  .then(() => {
    console.log('D');
  });

console.log('E');

Thứ tự đúng là: A, E, C, D, B. Hai then liên tiếp vẫn nằm trong Microtask Queue và được chạy liền nhau trước khi Event Loop quay lại xử lý Macrotask.

6. Kết nối với performance thực tế

Khi debug performance, bạn sẽ thấy trong Performance tab của DevTools có các phase như "scripting", "rendering", "painting". Rất nhiều vấn đề không nằm ở chỗ "JS chậm" mà là do ta block event loop quá lâu.

  • Tránh vòng lặp nặng trong 1 tick.
  • Ưu tiên I/O async.
  • Tận dụng Web Worker khi xử lý CPU bound (ví dụ: encode video, xử lý ảnh).

Nắm chắc Event Loop là bước ngoặt để từ một coder trở thành một Engineer thực thụ, biết cách đọc stack trace, hiểu timeline và tối ưu ứng dụng trên cả frontend lẫn backend.