- JavaScript chỉ có một luồng thực thi nhưng không bao giờ bị chặn - nhờ event loop điều phối giữa call stack, Web APIs, task queue và microtask queue.
- Promise luôn chạy trước setTimeout vì microtask queue được drain hoàn toàn trước mỗi macrotask tiếp theo.
- Hiểu sai quy tắc này là nguồn gốc của hàng loạt bug thứ tự thực thi khó chịu trong production.
- freeCodeCamp vừa publish khóa học miễn phí giải thích toàn bộ cơ chế qua animation bước-từng-bước.
TL;DR

JavaScript chạy trên một luồng duy nhất nhưng vẫn xử lý được hàng trăm tác vụ bất đồng bộ - fetch, timer, event - mà không bị chặn. Cơ chế đứng sau đó là event loop, phối hợp giữa call stack, Web APIs, task queue và microtask queue. freeCodeCamp vừa ra khóa học Mastering the JavaScript Event Loop giải thích toàn bộ cơ chế này qua animation và ví dụ bước-từng-bước - miễn phí, dành cho lập trình viên muốn đi từ "biết dùng async" sang "hiểu tại sao nó hoạt động".
Vấn đề cốt lõi: JS chỉ có một luồng
Không giống Python hay Java có thể tạo nhiều thread, JavaScript runtime chỉ có một call stack duy nhất. Mọi hàm được đưa vào stack theo thứ tự LIFO - thực thi xong rồi mới bật ra. Điều này có nghĩa: nếu một hàm chạy lâu (vòng lặp 10 triệu phần tử, tính toán nặng), toàn bộ trang web đóng băng - không click được, không render được.
Vậy làm sao fetch(), setTimeout(), hay DOM events vẫn hoạt động "song song"? Câu trả lời: chúng không chạy trong JS runtime mà chạy trong Web APIs của browser - được viết bằng C++, có khả năng đa luồng thực sự. Kết quả của chúng chỉ quay lại JS thông qua các hàng đợi callback.
Bốn thành phần bạn cần nắm
Đây là bốn mảnh ghép tạo nên async JavaScript:
- Call Stack: LIFO, một luồng, nơi code đồng bộ thực thi. Khi stack rỗng, event loop mới kéo task tiếp theo vào.
- Web APIs: timer, fetch, DOM events - chạy song song bên ngoài JS engine, callback được đẩy vào queue khi hoàn thành.
- Task Queue (Macrotask Queue): hàng đợi FIFO cho callback của
setTimeout,setInterval, I/O events. Mỗi vòng event loop chỉ lấy một macrotask. - Microtask Queue: hàng đợi ưu tiên cao hơn, chứa callback của Promise (
.then/.catch/.finally),async/await,MutationObserver, vàqueueMicrotask(). Sau mỗi macrotask, event loop drain toàn bộ microtask queue trước khi làm bất cứ điều gì khác.
Tại sao Promise luôn chạy trước setTimeout
Đây là câu phỏng vấn kinh điển. Thử đoán output của đoạn này trước khi chạy:
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');
Output: Start → End → Promise → Timeout
Thuật toán event loop hoạt động theo thứ tự này mỗi vòng lặp:
- Thực thi hết code đồng bộ hiện tại trong call stack
- Drain toàn bộ microtask queue (kể cả microtask mới được thêm vào trong bước này)
- Render nếu browser cần cập nhật UI
- Lấy một macrotask từ task queue
- Lặp lại từ bước 1
setTimeout(..., 0) chỉ nghĩa là "thêm vào macrotask queue sớm nhất có thể" - không phải chạy ngay. Browser còn áp đặt delay tối thiểu ~4ms với nested setTimeout. Promise callbacks vào microtask queue và luôn được drain trước khi macrotask tiếp theo chạy.
Rủi ro starvation: nếu một promise liên tục enqueue thêm promise mới, macrotask queue sẽ không bao giờ được xử lý - render bị chặn, setTimeout không bao giờ chạy. Đây là lỗi kiến trúc thực trong các hệ thống dùng promise chain nặng.
Sai lầm lập trình viên hay mắc
- "setTimeout(fn, 0) chạy ngay": sai. Nó phải chờ stack rỗng + tất cả microtask xong + render, rồi mới được xử lý.
- "async/await offload CPU": sai. Code đồng bộ bên trong hàm async vẫn chặn call stack.
awaitchỉ suspend hàm async, không tạo thread mới. - "Delay của setTimeout là chính xác": sai. 1000ms là thời gian sớm nhất callback có thể chạy - nếu có macrotask chạy trước đó, bị trễ thêm.
- "Promise và setTimeout cùng mức ưu tiên": nhầm lẫn phổ biến nhất, dẫn đến bug thứ tự thực thi rất khó debug trong production.
Học từ đâu
freeCodeCamp vừa publish khóa học Mastering the JavaScript Event Loop với animation minh họa từng bước hoạt động của call stack, Web APIs, task queue và microtask queue. Phù hợp để nâng cấp từ "biết dùng async/await" lên "hiểu tại sao code chạy theo thứ tự đó".
Để đào sâu hơn: javascript.info/event-loop giải thích thuật toán ở mức spec; Lydia Hallie's visual guide dành cho người học qua hình ảnh; MDN Microtask Guide nếu bạn cần dùng queueMicrotask() trong thực tế.
Nguồn: freeCodeCamp, javascript.info, MDN.



