Họ đang đo gì
Bạn có mô hình thật về cách runtime lập lịch, hay chỉ học thuộc “promise chạy trước setTimeout”.
Trả lời ngắn~30 giây
Code đồng bộ chạy hết trước. Rồi call stack rỗng, engine vét TOÀN BỘ microtask queue — promise callback, queueMicrotask, phần sau await — kể cả microtask sinh ra trong lúc vét. Chỉ sau khi queue đó rỗng mới lấy MỘT macrotask (setTimeout, sự kiện I/O, sự kiện DOM), rồi lại vét microtask. Nên Promise.then luôn chạy trước setTimeout(…, 0), dù setTimeout được đăng ký trước.
Chương trình bắt đầu. Toàn bộ code đồng bộ chạy tới hết trước khi bất cứ thứ gì trong hàng đợi được đụng tới.
Giải thích sâu
Cách hình dung có ích: microtask là “việc phải xong trước khi trình duyệt được phép thở”, macrotask là “việc xếp lịch cho lượt sau”. Vì mọi microtask phải xong trước khi render, một vòng lặp microtask vô tận sẽ treo tab hoàn toàn — trang không vẽ lại, không nhận sự kiện. Cùng đoạn code viết bằng setTimeout đệ quy thì tab vẫn phản hồi, chỉ chậm. Đây là khác biệt có hậu quả thật, không phải chi tiết học thuật.
await là chỗ hay gây bất ngờ. Nó không “dừng chương trình”: nó cắt hàm async làm đôi, chạy phần trước ngay lập tức và ĐĂNG KÝ phần sau như một microtask. Vì vậy await một giá trị đã sẵn sàng vẫn tốn một lượt microtask, và thứ tự log đổi khác so với gọi .then() trực tiếp trong vài trường hợp tinh vi.
Một điểm ít người biết mà hỏi rất hay: thứ tự này không nằm trong đặc tả ECMAScript. ECMAScript chỉ định nghĩa hàng đợi job cho promise; hàng đợi task, thời điểm render và cả setTimeout đều do đặc tả HTML định nghĩa. Đó là lý do Node.js có thứ tự khác trình duyệt ở vài chỗ — nó có process.nextTick chen trước microtask, và các phase riêng của libuv.
Thứ tự in ra là gì?
console.log('1');
setTimeout(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
(async () => {
console.log('5');
await null;
console.log('6');
})();
console.log('7');1 5 7 3 4 6 2 — đồng bộ trước (1, 5, 7), rồi vét microtask theo thứ tự đăng ký (3, 4, 6), cuối cùng mới tới macrotask (2).
Câu hỏi tiếp theo họ sẽ hỏi
?setTimeout(fn, 0) có chạy sau đúng 0ms không?
Không. Đặc tả HTML ghim mức tối thiểu 4ms sau bốn tầng lồng nhau, và ngoài ra callback còn phải chờ tới lượt trong task queue. Nếu có một hàm đồng bộ chạy 2 giây thì timeout của bạn nằm im 2 giây.
?Node.js khác trình duyệt chỗ nào?
Node có process.nextTick với độ ưu tiên CAO HƠN cả microtask promise, và vòng lặp sự kiện chia thành các phase của libuv (timers, poll, check…), nên setImmediate với setTimeout(…, 0) có thể đổi thứ tự cho nhau tuỳ ngữ cảnh. Trong trình duyệt thì không có hai thứ đó.
?Vì sao vòng lặp microtask vô tận treo trang mà setTimeout đệ quy thì không?
Vì render chỉ xảy ra sau khi microtask queue rỗng. Queue không bao giờ rỗng thì trình duyệt không bao giờ tới bước vẽ. Còn mỗi setTimeout là một macrotask riêng, giữa hai lượt trình duyệt có cơ hội vẽ và xử lý sự kiện.
Trả lời thế này là mất điểm
- Nói “JavaScript đa luồng nhờ async”. Async không tạo luồng; nó chỉ sắp xếp công việc trên một luồng duy nhất.
- Học thuộc “promise trước timeout” mà không giải thích được vì sao, nên sai ngay khi câu hỏi thêm
awaitlồng nhau.
Trả lời thế này là ghi điểm
- Nói được rằng render nằm giữa các macrotask, nên tính toán nặng nên cắt nhỏ theo macrotask chứ không phải microtask.