Xử lý bất đồng bộ trong JavaScript
Javascript nâng cao | by
Trong JavaScript, bất đồng bộ (asynchronous) là một cơ chế quan trọng giúp chương trình có thể thực thi nhiều tác vụ mà không bị chặn (blocking). Điều này đặc biệt hữu ích khi làm việc với các thao tác mất thời gian như gọi API, đọc/ghi file, hoặc xử lý sự kiện người dùng.
JavaScript là một ngôn ngữ đơn luồng (single-threaded), có nghĩa là nó chỉ có thể thực thi một đoạn mã tại một thời điểm. Nếu không có cơ chế bất đồng bộ, các tác vụ nặng sẽ làm chương trình bị treo và không thể phản hồi. Để giải quyết vấn đề này, JavaScript sử dụng các cơ chế như callback, Promise, và async/await để xử lý bất đồng bộ một cách hiệu quả.
Trong bài viết này, chúng ta sẽ tìm hiểu về khái niệm bất đồng bộ, cách nó hoạt động, các kỹ thuật xử lý bất đồng bộ, cũng như các ứng dụng thực tế và cách khắc phục các vấn đề thường gặp.
Bất đồng bộ trong JavaScript là gì?
Bất đồng bộ (asynchronous) trong JavaScript là một cơ chế giúp chương trình có thể thực thi nhiều tác vụ mà không cần chờ một tác vụ hoàn thành trước khi bắt đầu tác vụ tiếp theo.
Trong lập trình đồng bộ (synchronous), các dòng lệnh được thực thi theo thứ tự từ trên xuống dưới. Nếu một tác vụ mất thời gian (như tải dữ liệu từ API hoặc đọc file), chương trình sẽ bị chặn (blocking) cho đến khi tác vụ đó hoàn tất. Điều này có thể làm chậm ứng dụng và gây ra trải nghiệm người dùng không mượt mà.
Với lập trình bất đồng bộ, thay vì chờ một tác vụ hoàn thành, JavaScript có thể tiếp tục thực thi các tác vụ khác và chỉ xử lý kết quả khi tác vụ bất đồng bộ hoàn tất. Điều này giúp cải thiện hiệu suất và tối ưu hóa trải nghiệm người dùng.
Cách hoạt động của bất đồng bộ trong JavaScript
JavaScript là một ngôn ngữ đơn luồng (single-threaded), nghĩa là nó chỉ có một luồng thực thi duy nhất. Để hỗ trợ bất đồng bộ, JavaScript sử dụng Event Loop, Web APIs, và Callback Queue để quản lý các tác vụ không đồng bộ.
Cơ chế hoạt động như sau:
-
Khi một hàm bất đồng bộ (như
setTimeout
,fetch
, hoặc sự kiệnclick
) được gọi, JavaScript chuyển nó vào Web APIs để xử lý mà không làm gián đoạn luồng chính. -
Trong khi Web APIs xử lý tác vụ, luồng chính tiếp tục thực thi các lệnh tiếp theo.
-
Khi tác vụ bất đồng bộ hoàn tất, nó được đưa vào hàng đợi (Callback Queue hoặc Microtask Queue).
-
Event Loop kiểm tra nếu luồng chính đã rảnh, nó sẽ lấy tác vụ từ hàng đợi và thực thi.
Ví dụ về bất đồng bộ trong thực tế
Ví dụ 1: Sử dụng setTimeout
để mô phỏng bất đồng bộ
console.log("Bắt đầu"); setTimeout(() => { console.log("Tác vụ bất đồng bộ đã hoàn thành!"); }, 2000); console.log("Kết thúc");
Kết quả đầu ra:
Bắt đầu Kết thúc Tác vụ bất đồng bộ đã hoàn thành!
Mặc dù setTimeout
được gọi trước console.log("Kết thúc")
, nhưng do nó là bất đồng bộ, chương trình không chờ 2 giây mà tiếp tục thực thi lệnh tiếp theo. Sau khi 2 giây trôi qua, callback của setTimeout
mới được thực thi.
Ví dụ 2: Gọi API bất đồng bộ với fetch
console.log("Bắt đầu gọi API..."); fetch("https://jsonplaceholder.typicode.com/todos/1") .then(response => response.json()) .then(data => console.log("Dữ liệu từ API:", data)); console.log("Gọi API hoàn tất (chưa nhận dữ liệu)");
Kết quả đầu ra:
Bắt đầu gọi API... Gọi API hoàn tất (chưa nhận dữ liệu) Dữ liệu từ API: {userId: 1, id: 1, title: "...", completed: false}
Mặc dù JavaScript đã gửi yêu cầu API, nhưng thay vì chờ phản hồi, chương trình tiếp tục thực thi lệnh tiếp theo. Khi API trả về dữ liệu, then()
mới xử lý kết quả.
Cơ chế hoạt động của bất đồng bộ trong JavaScript
JavaScript là một ngôn ngữ đơn luồng (single-threaded), có nghĩa là nó chỉ có một luồng thực thi duy nhất. Để hỗ trợ các tác vụ bất đồng bộ, JavaScript sử dụng một mô hình xử lý dựa trên Call Stack, Web APIs, Task Queue, và Event Loop.
Call Stack (Ngăn xếp gọi hàm)
Call Stack là nơi JavaScript thực thi các lệnh theo cơ chế LIFO (Last In, First Out), nghĩa là hàm được gọi sau cùng sẽ được thực thi trước.
Ví dụ: Hoạt động của Call Stack với các hàm đồng bộ
function greet() { console.log("Hello"); } function sayGoodbye() { console.log("Goodbye"); } greet(); sayGoodbye();
Quá trình thực thi của Call Stack:
-
greet()
được đưa vào Call Stack và thực thi (console.log("Hello")
). -
Sau khi
greet()
kết thúc, nó được xóa khỏi Call Stack. -
sayGoodbye()
được đưa vào Call Stack và thực thi (console.log("Goodbye")
). -
sayGoodbye()
kết thúc và bị xóa khỏi Call Stack.
Kết quả in ra:
Hello Goodbye
Với các hàm đồng bộ, Call Stack xử lý tuần tự từng hàm một. Nhưng với các tác vụ bất đồng bộ, Call Stack sẽ không chờ mà tiếp tục xử lý các lệnh khác.
Web APIs (Các API trình duyệt hỗ trợ bất đồng bộ) trong JavaScript
Khi JavaScript gặp một hàm bất đồng bộ (như setTimeout
, fetch
, hoặc addEventListener
), nó sẽ chuyển tác vụ đó sang Web APIs (do trình duyệt cung cấp) thay vì giữ nó trong Call Stack.
Ví dụ:
console.log("Bắt đầu"); setTimeout(() => { console.log("Đã hoàn thành!"); }, 2000); console.log("Kết thúc");
Cách Web APIs xử lý:
-
console.log("Bắt đầu")
→ Đưa vào Call Stack, thực thi, rồi bị xóa. -
setTimeout(() => { console.log("Đã hoàn thành!"); }, 2000);
-
Call Stack gửi
setTimeout
đến Web APIs. -
Web APIs bắt đầu đếm 2 giây mà không chặn chương trình.
-
-
console.log("Kết thúc")
→ Đưa vào Call Stack, thực thi, rồi bị xóa. -
Sau 2 giây, Web APIs đưa callback (
console.log("Đã hoàn thành!")
) vào Task Queue chờ xử lý.
Kết quả in ra:
Bắt đầu Kết thúc Đã hoàn thành!
Web APIs giúp xử lý các tác vụ bất đồng bộ mà không làm chậm chương trình.
Task Queue (Hàng đợi tác vụ)
Khi một tác vụ bất đồng bộ hoàn thành, nó sẽ được đưa vào Task Queue (hoặc Microtask Queue đối với Promise
).
Task Queue chứa các callback từ Web APIs chờ được thực thi.
-
Task Queue không thực thi ngay mà chờ đến khi Call Stack trống.
Ví dụ:
console.log("1"); setTimeout(() => { console.log("2"); }, 0); console.log("3");
Quá trình thực thi:
-
console.log("1")
→ Thực thi ngay. -
setTimeout(..., 0)
→ Chuyển callback vào Web APIs. -
console.log("3")
→ Thực thi ngay. -
Callback từ
setTimeout
được đưa vào Task Queue và đợi Call Stack trống. -
Khi Call Stack trống, Event Loop lấy callback từ Task Queue và thực thi.
Kết quả in ra:
1 3 2
Mặc dù setTimeout đặt thời gian là 0ms, nhưng callback vẫn phải đợi Call Stack trống mới được thực thi.
Event Loop (Vòng lặp sự kiện giúp xử lý bất đồng bộ)
Event Loop là cơ chế giúp JavaScript kiểm tra Call Stack và Task Queue để quyết định khi nào thực thi các callback bất đồng bộ.
Cách hoạt động của Event Loop:
-
Nếu Call Stack có hàm đang chạy → Event Loop chờ.
-
Nếu Call Stack trống → Event Loop lấy tác vụ từ Task Queue đưa vào Call Stack để thực thi.
-
Lặp lại quá trình trên liên tục.
Sự khác biệt giữa Task Queue và Microtask Queue:
-
Task Queue: Chứa các callback từ
setTimeout
,setInterval
,event listeners
. -
Microtask Queue: Chứa các callback từ
Promise.then()
,async/await
,MutationObserver
.
Microtask Queue có độ ưu tiên cao hơn Task Queue, nghĩa là nó sẽ được thực thi trước khi Task Queue.
Ví dụ:
console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");
Thứ tự thực thi:
-
console.log("A")
→ Call Stack → Thực thi ngay. -
setTimeout(...)
→ Chuyển callback vào Web APIs. -
Promise.resolve().then(...)
→ Đưa callback vào Microtask Queue. -
console.log("D")
→ Call Stack → Thực thi ngay. -
Microtask Queue được thực thi trước → console.log("C").
-
Call Stack trống, Event Loop lấy từ Task Queue →
console.log("B")
.
Kết quả in ra:
A D C B
Promise (C) chạy trước setTimeout (B) vì Microtask Queue có độ ưu tiên cao hơn Task Queue.
Ví dụ minh họa về cách JavaScript xử lý bất đồng bộ
console.log("1. Bắt đầu"); setTimeout(() => console.log("2. setTimeout"), 0); Promise.resolve().then(() => console.log("3. Promise")); console.log("4. Kết thúc");
Thứ tự thực thi:
-
console.log("1. Bắt đầu")
→ Call Stack → Thực thi ngay. -
setTimeout(...)
→ Đưa callback vào Web APIs. -
Promise.resolve().then(...)
→ Đưa callback vào Microtask Queue. -
console.log("4. Kết thúc")
→ Call Stack → Thực thi ngay. -
Microtask Queue thực thi trước Task Queue →
console.log("3. Promise")
. -
Call Stack trống, Event Loop lấy từ Task Queue →
console.log("2. setTimeout")
.
Kết quả in ra:
1. Bắt đầu 4. Kết thúc 3. Promise 2. setTimeout
Các kỹ thuật xử lý bất đồng bộ trong JavaScript
JavaScript cung cấp nhiều kỹ thuật để xử lý các tác vụ bất đồng bộ, giúp cải thiện hiệu suất và khả năng đọc hiểu mã nguồn. Ba phương pháp phổ biến nhất là Callback, Promise, và Async/Await.
Callback là gì?
Callback là một hàm được truyền vào một hàm khác dưới dạng đối số và sẽ được thực thi khi tác vụ bất đồng bộ hoàn thành.
Ví dụ đơn giản về Callback: