Cách Event Loop quản lý các tác vụ bất đồng bộ trong Javascript

Cách Event Loop quản lý các tác vụ bất đồng bộ trong Javascript

Mở đầu

JavaScript là một ngôn ngữ lập trình chạy theo mô hình đơn luồng (single-threaded), điều này nghe có vẻ như bất kỳ tác vụ nào chạy quá lâu sẽ ngăn chặn các tác vụ phía sau, gây ra tình trạng treo cho trang web. Tuy nhiên, khi sử dụng và tương tác với các website trên trình duyệt, chúng ta vẫn thấy rằng mọi thứ luôn hoạt động mượt mà.

Vậy đâu là lý do cho điều này? Có một cơ chế độc đáo để quản lý các tác vụ mà không gây tắc nghẽn hệ thống, và đó chính là Event Loop.

Event Loop là gì?

Event Loop là một thành phần thuộc Javascript Runtime Environment, giúp quản lý và thực thi các tác vụ một cách bất đồng bộ mà không cần phải chờ đợi lẫn nhau. Chúng ta hãy cùng tìm hiểu chi tiết hơn về JavaScript Runtime Environment, cũng như cách thức hoạt động và vai trò của Event Loop trong hệ thống này.

JavaScript Runtime Environment

JavaScript Runtime Environment là nơi xử lý và thực thi các tác vụ liên quan đến JavaScript. Để JavaScript có thể thực thi một cách mượt mà, cần đến những thành phầnหลัก sau:

  1. JavaScript Engine: Đóng vai trò như trình biên dịch, giúp dịch mã JavaScript thành mã nhị phân mà máy tính có thể hiểu và thực thi. Bạn có thể tìm hiểu thêm về V8 Engine - trong Chrome và Node.js tại đây. Tuy nhiên, JavaScript Engine chỉ có khả năng xử lý mã thuần theo cách đồng bộ, tức là theo nguyên lý hàng đợi (stack).
// Một số ví dụ mã thuần mà JavaScript Engine có thể xử lý:
console.log("Learning JavaScript");
const a = 1;
  1. Web / Browser API: Đây là các API được cung cấp bởi môi trường runtime như Browser và Node.js, giúp JavaScript giao tiếp với các thành phần bên ngoài.

2.1. DOM (Document Object Model)

DOM là tập hợp các phương thức giúp JavaScript giao tiếp và tương tác với HTML/CSS. Các phương thức này thực thi một cách đồng bộ.

// Giao tiếp với DOM
document.getElementById("mydiv").innerHTML = "Hello Viblo!!!";
document.getElementById("mydiv").style.color = "blue";

2.2. Web Storage API

Web Storage API cho phép lưu trữ dữ liệu trên trình duyệt với hai dạng chính:

| Loại | Chức năng | |-----------------|--------------------------------------------| | localStorage | Lưu dữ liệu vĩnh viễn, không bị mất khi đóng trình duyệt. | | sessionStorage | Lưu dữ liệu tạm thời, bị mất khi đóng tab trình duyệt. |

// Lưu trữ dữ liệu với localStorage và sessionStorage
localStorage.setItem("username", "ThanhEtn");
sessionStorage.setItem("token", "abc123");

2.3. Web Worker API

Web Worker giúp xử lý các công việc nặng trong background thread (một luồng riêng biệt), giúp giữ cho main thread (giao diện người dùng) không bị chặn.

Các tác vụ mà Web Worker có thể xử lý hiệu quả gồm có:

| Tác vụ | Mô tả | |-------------------------------------|----------------------------------------| | Xử lý tính toán phức tạp | Tính toán số nguyên tố, xử lý dữ liệu lớn. | | Gọi API & xử lý dữ liệu | Fetch, WebSockets, IndexedDB, Timer API. |

// Ví dụ về Web Worker
async function getData () {
    const response = await fetch("https://viblo.asia/api/posts/newest?limit=20");
    return await response.json();
}

// Sử dụng Timer API
console.log("Chạy trong Main Thread");
setTimeout(() => {
    console.log("Đã chờ 3 giây trong Web Worker!");
}, 3000);

Task Queue / MicroTask Queue

Các tác vụ bất đồng bộ sau khi thực thi xong sẽ được đưa vào các hàng đợi đặc biệt: Task QueueMicroTask Queue.

  • Task Queue: Thực thi theo quy tắc FIFO (First In, First Out), tức là tác vụ nào được đưa vào hàng đợi trước sẽ được thực thi trước.
// Ví dụ về Task Queue
setTimeout(() => console.log("Task 1"), 0);
setTimeout(() => console.log("Task 2"), 0);
  • MicroTask Queue: Có thứ tự ưu tiên cao hơn, tức là các tác vụ trong đây sẽ được thực hiện trước khi đến Task Queue.
console.log("Start");
setTimeout(() => console.log("Task Queue"), 0);
Promise.resolve().then(() => console.log("MicroTask Queue"));
console.log("End");

Trình tự thực thi

| Trình tự thực thi | |----------------------------------------| | 1. "Start" → Call Stack (thực thi ngay) | | 2. setTimeout() → Gửi vào Task Queue | | 3. Promise.then() → Gửi vào MicroTask Queue | | 4. "End" → Call Stack (thực thi ngay) | | 5. MicroTask Queue chạy trước → "MicroTask Queue" | | 6. Sau khi MicroTask Queue trống → "Task Queue" |

Event Loop

Event Loop là cầu nối giữa Call Stack và các Task Queue, giúp đảm bảo các tác vụ bất đồng bộ được xử lý mượt mà dù JavaScript chỉ chạy trong một luồng duy nhất. Các nhiệm vụ chính của Event Loop gồm:

  1. Kiểm tra trạng thái của Call Stack. Nếu trống, Event Loop sẽ tìm một tác vụ mới từ Queue để đưa vào Call Stack.
  2. Ưu tiên lấy tác vụ từ MicroTask Queue trước.
  3. Khi MicroTask Queue đã rỗng, lấy từng tác vụ trong Task Queue vào Call Stack để thực thi.
Sơ đồ mô tả cách Event Loop hoạt động

Kết luận

JavaScript là một ngôn ngữ chạy đơn luồng, nhưng với kiến trúc tuyệt vời của JavaScript Runtime Environment, chúng ta có thể xử lý nhiều tác vụ một cách mượt mà. Nhờ vào sự kết hợp hoàn hảo giữa các thành phần, đặc biệt là Event Loop, chúng ta có thể điều phối hoạt động của các tác vụ đồng bộ và bất đồng bộ một cách hiệu quả. Hy vọng bài viết này giúp bạn có cái nhìn rõ nét hơn về cách thức hoạt động của JavaScript và lý do vì sao một ngôn ngữ đơn luồng lại có thể xử lý các tác vụ bất đồng bộ.