Ban biên tập CodeReady
Hiểu Sâu Về Cơ Chế Bất Đồng Bộ Trong JavaScript Cho Frontend Developer
Khám phá cách thức Event Loop, Task Queue, Microtask Queue và Promise hoạt động bên trong JavaScript để xây dựng ứng dụng Frontend hiệu năng cao và không bị nghẽn giao diện.
Trong phát triển ứng dụng Frontend hiện đại, JavaScript đóng vai trò trung tâm trong việc xử lý tương tác người dùng, gọi API và cập nhật giao diện. Do bản chất đơn luồng (single-threaded), việc nắm vững cơ chế bất đồng bộ (asynchronous) là chìa khóa để viết code mượt mà, tránh tình trạng đơ giật giao diện (jank).
1. Luồng Thực Thi Đơn Luồng và Event Loop
JavaScript thực thi mã trên một luồng duy nhất gọi là Call Stack. Điều này có nghĩa là tại một thời điểm, chỉ có một tác vụ được xử lý. Khi gặp các tác vụ tốn thời gian như gọi mạng (fetch) hay hẹn giờ (setTimeout), nếu xử lý đồng bộ, luồng chính sẽ bị khóa.
Để giải quyết vấn đề này, môi trường thực thi (Browser hoặc Node.js) cung cấp các Web APIs và Event Loop. Event Loop liên tục kiểm tra xem Call Stack có trống hay không. Nếu trống, nó sẽ lấy các tác vụ từ hàng đợi (Queue) để đưa vào Call Stack thực thi.
2. Phân Biệt Microtask Queue và Macrotask Queue
Không phải tất cả các tác vụ bất đồng bộ đều có độ ưu tiên giống nhau. JavaScript phân chia chúng thành hai hàng đợi chính:
- Microtask Queue: Chứa các tác vụ có độ ưu tiên cao như
Promise.then/catch/finally,queueMicrotask, vàMutationObserver. - Macrotask Queue (Task Queue): Chứa các tác vụ như
setTimeout,setInterval,setImmediate, và các sự kiện I/O.
Quy tắc vàng của Event Loop: Sau khi một đoạn mã đồng bộ (script) chạy xong, Event Loop sẽ thực thi toàn bộ các tác vụ trong Microtask Queue cho đến khi sạch bóng, trước khi chuyển sang chạy một tác vụ đơn lẻ trong Macrotask Queue.
console.log('1: Đồng bộ');
setTimeout(() => {
console.log('2: Macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Microtask (Promise)');
});
console.log('4: Đồng bộ');Thứ tự in ra màn hình sẽ là: 1, 4, 3, và cuối cùng là 2. Lý do là Microtask (Promise) luôn chạy trước Macrotask (setTimeout).
3. Best Practice Khi Xử Lý Bất Đồng Bộ Trên Frontend
Để tối ưu hóa trải nghiệm người dùng và hiệu năng ứng dụng, Frontend Developer cần tuân thủ các nguyên tắc sau:
- Tránh Blocking Call Stack: Không thực hiện các vòng lặp nặng (như vòng lặp tính toán lớn) trực tiếp trên luồng chính. Hãy chia nhỏ dữ liệu hoặc sử dụng Web Workers.
- Sử Dụng Async/Await: Cú pháp
async/awaitgiúp code bất đồng bộ trông giống như đồng bộ, dễ đọc và dễ bảo trì hơn so với callback hell hay promise chaining thuần túy. - Xử Lý Lỗi Triệt Để: Luôn sử dụng
try...catchkhi dùngasync/awaitđể tránh các unhandled promise rejection làm sập ứng dụng ngầm.
4. Các Lỗi Thường Gặp
Trong quá trình phỏng vấn và thực tế lập trình, các lỗi sau thường xuyên xuất hiện:
- Quên return Promise: Khi viết hàm wrapper hoặc gọi các hàm bất đồng bộ lồng nhau mà quên từ khóa
return, dẫn đến việc hàm bên ngoài nhận vềundefined. - Hiểu sai thời gian của setTimeout:
setTimeout(fn, 1000 không có nghĩa là hàm fn sẽ chạy chính xác sau 1 giây, mà là nó sẽ được đưa vào Macrotask Queue sau ít nhất 1 giây, tùy thuộc vào việc Call Stack có đang bận hay không. - Race Condition: Gọi nhiều API cùng lúc nhưng API trả về sau lại ghi đè kết quả của API trả về trước (ví dụ trong tính năng tìm kiếm gõ nhanh - autocomplete).
5. Câu Hỏi Phỏng Vấn Thường Gặp (FAQ)
Q: Tại sao JavaScript lại là đơn luồng mà vẫn xử lý được bất đồng bộ?
A: JavaScript bản thân nó là đơn luồng (chỉ có một Call Stack), nhưng môi trường chạy (như trình duyệt) cung cấp các Web APIs đa luồng ngầm để xử lý các tác vụ I/O, sau đó đẩy callback về Event Loop của JavaScript.
Q: Sự khác biệt lớn nhất giữa Promise và Observable là gì?
A: Promise xử lý một giá trị duy nhất trong tương lai và không thể hủy (eager). Observable trong RxJS xử lý một luồng (stream) gồm nhiều giá trị theo thời gian, có thể hủy (lazy) và cung cấp nhiều toán tử xử lý dữ liệu phong phú hơn.
Checklist Kiểm Tra Code Bất Đồng Bộ
- [ ] Đã bọc
awaittrong khốitry...catchchưa? - [ ] Các lời gọi API độc lập đã được tối ưu bằng
Promise.allthay vì gọi tuần tự chưa? - [ ] Đã hủy các subscription hoặc yêu cầu fetch không cần thiết (dùng
AbortController) khi component bị unmount chưa?