Blogs
Deep dive vào Event Loop và bất đồng bộ trong JavaScript

Deep dive vào Event Loop và bất đồng bộ trong JavaScript

Tìm hiểu cách Event Loop hoạt động và cách xử lý bất đồng bộ trong JavaScript.

1. Event Loop là gì?

JavaScript là một ngôn ngữ đơn luồng (single-threaded), có nghĩa là nó chỉ có một luồng chính để thực thi code. Để xử lý tác vụ bất đồng bộ (asynchronous) mà không chặn chương trình, JavaScript sử dụng Event Loop.

Event Loop là cơ chế giúp JavaScript quản lý các tác vụ bất đồng bộ như setTimeout, fetch, event listener, và promises bằng cách điều phối các hàng đợi (queues) khác nhau.


2. Call Stack, Web APIs, Task Queue và Microtask Queue

Call Stack (Ngăn xếp gọi hàm)

Call Stack là nơi JavaScript thực thi các hàm theo nguyên tắc LIFO (Last In, First Out).

function foo() { console.log("Foo"); } function bar() { foo(); } bar();

Quá trình thực thi:

  1. bar() được đưa vào Call Stack.
  2. bar() gọi foo(), foo() được đưa vào Call Stack.
  3. foo() thực thi xong, bị pop khỏi Call Stack.
  4. bar() thực thi xong, bị pop khỏi Call Stack.

Web APIs

Các hàm như setTimeout, fetch, và event listeners không chạy trong Call Stack mà được đưa đến Web APIs.

console.log("Start"); setTimeout(() => console.log("Timeout"), 0); console.log("End");

Output:

Start
End
Timeout

setTimeout có thời gian 0ms, nó vẫn không chạy ngay lập tức mà được đưa vào Web APIs rồi Task Queue.


Task Queue (Macro-task Queue)

Task Queue chứa các macro-task như:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O Callbacks

Event Loop chỉ lấy một task từ Task Queue mỗi lần và đưa vào Call Stack nếu Call Stack trống.


Microtask Queue (Priority Queue)

Microtask Queue có độ ưu tiên cao hơn Task Queue, chứa:

  • Promises (resolve, reject)
  • MutationObserver
  • queueMicrotask
console.log("Start"); setTimeout(() => console.log("Timeout"), 0); Promise.resolve().then(() => console.log("Promise")); console.log("End");

Output:

Start
End
Promise
Timeout

Microtask (Promise.resolve()) luôn chạy trước Task (setTimeout).


3. Cách Event Loop vận hành

  1. Thực thi Call Stack (synchronous code).
  2. Xử lý tất cả microtasks (Promise, queueMicrotask, MutationObserver).
  3. Xử lý một task từ Task Queue (setTimeout, I/O, events).
  4. Lặp lại quá trình trên.

Event Loop tiếp tục chạy cho đến khi không còn code nào để xử lý.


4. Async/Await và Event Loop

async/await giúp code bất đồng bộ dễ đọc hơn nhưng vẫn tuân theo Event Loop.

async function fetchData() { console.log("Start Fetch"); await new Promise(resolve => setTimeout(resolve, 1000)); console.log("End Fetch"); } console.log("Before Fetch"); fetchData(); console.log("After Fetch");

Output:

Before Fetch
Start Fetch
After Fetch
End Fetch

Giải thích:

  1. fetchData() gọi console.log("Start Fetch") → Call Stack.
  2. await tạm dừng fetchData(), trả control lại Event Loop.
  3. console.log("After Fetch") chạy.
  4. Sau 1s, console.log("End Fetch") chạy.

5. Tổng kết

JavaScript là single-threaded nhưng có thể xử lý bất đồng bộ nhờ Event Loop.
Microtasks (Promises) luôn chạy trước Macro-tasks (setTimeout, I/O).
Async/Await giúp xử lý bất đồng bộ dễ đọc hơn nhưng không thay đổi cơ chế Event Loop.

Hiểu rõ Event Loop giúp bạn tối ưu hiệu suất ứng dụng JavaScript và tránh các lỗi liên quan đến bất đồng bộ! 🚀

Deep Dive into Event Loop and Asynchronous Programming in JavaScript

Understand how the Event Loop works and how to handle asynchronous operations in JavaScript.

1. What is the Event Loop?

JavaScript is a single-threaded language, meaning it has only one main thread to execute code. To handle asynchronous tasks without blocking the program, JavaScript uses the Event Loop.

The Event Loop is the mechanism that allows JavaScript to manage asynchronous tasks like setTimeout, fetch, event listeners, and promises by coordinating different queues.


2. Call Stack, Web APIs, Task Queue, and Microtask Queue

Call Stack

The Call Stack is where JavaScript executes functions based on the LIFO (Last In, First Out) principle.

function foo() { console.log("Foo"); } function bar() { foo(); } bar();

Execution flow:

  1. bar() is pushed onto the Call Stack.
  2. bar() calls foo(), so foo() is pushed onto the Call Stack.
  3. Once foo() completes, it is popped off the Call Stack.
  4. Once bar() completes, it is popped off the Call Stack.

Web APIs

Functions like setTimeout, fetch, and event listeners don't run in the Call Stack; they are handed off to Web APIs.

console.log("Start"); setTimeout(() => console.log("Timeout"), 0); console.log("End");

Output:

Start
End
Timeout

Even though setTimeout has a 0ms delay, it doesn’t run immediately but is handed off to the Web APIs and then the Task Queue.


Task Queue (Macro-task Queue)

The Task Queue contains macro-tasks such as:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O Callbacks

The Event Loop only processes one task from the Task Queue at a time and pushes it into the Call Stack if the Call Stack is empty.


Microtask Queue (Priority Queue)

The Microtask Queue has higher priority than the Task Queue and contains:

  • Promises (resolve, reject)
  • MutationObserver
  • queueMicrotask
console.log("Start"); setTimeout(() => console.log("Timeout"), 0); Promise.resolve().then(() => console.log("Promise")); console.log("End");

Output:

Start
End
Promise
Timeout

Microtasks (Promise.resolve()) always run before Tasks (setTimeout).


3. How the Event Loop Works

  1. Execute the Call Stack (synchronous code).
  2. Process all microtasks (Promise, queueMicrotask, MutationObserver).
  3. Process one task from the Task Queue (setTimeout, I/O, events).
  4. Repeat the process.

The Event Loop continues running until there is no more code to process.


4. Async/Await and the Event Loop

async/await makes asynchronous code more readable but still follows the Event Loop mechanism.

async function fetchData() { console.log("Start Fetch"); await new Promise(resolve => setTimeout(resolve, 1000)); console.log("End Fetch"); } console.log("Before Fetch"); fetchData(); console.log("After Fetch");

Output:

Before Fetch
Start Fetch
After Fetch
End Fetch

Explanation:

  1. fetchData() calls console.log("Start Fetch") → Call Stack.
  2. await pauses fetchData(), handing control back to the Event Loop.
  3. console.log("After Fetch") runs.
  4. After 1 second, console.log("End Fetch") runs.

5. Summary

JavaScript is single-threaded but can handle asynchronous tasks with the Event Loop.
Microtasks (Promises) always run before Macro-tasks (setTimeout, I/O).
Async/Await makes asynchronous code more readable but does not change the Event Loop mechanism.

Understanding the Event Loop helps you optimize JavaScript application performance and avoid issues related to asynchronous code! 🚀

Tag

Buy Me A Coffee
    JavaScript