Câu hỏi “đoạn code này in ra gì?” không chỉ kiểm tra trí nhớ. Người phỏng vấn thường muốn biết bạn phân biệt được phần chạy ngay, công việc được xếp hàng và môi trường thực thi hay không. Nếu chỉ đọc đáp án A, B, C, bạn có thể lúng túng khi thay một dòng code nhỏ.
Bài viết dùng ví dụ JavaScript độc lập, không có framework hay I/O bên ngoài. Tài liệu MDN được đối chiếu ngày 22/10/2026. Để thử bằng Node, ghi lại node --version; không suy kết quả về thứ tự các API đặc thù Node, trình duyệt và runtime mobile nếu chưa kiểm tra riêng.
Bài tập đầu tiên
Trước khi chạy, tự ghi thứ tự dự đoán và lý do của từng dòng:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
async function run() {
console.log('D');
await null;
console.log('E');
}
run();
console.log('F');
Trong tình huống độc lập này, thứ tự cần giải thích là:
A
D
F
C
E
B
Điểm dễ bỏ sót là run() bắt đầu chạy ngay. Từ khóa async không đẩy toàn bộ thân hàm sang một thời điểm khác. Phần trước await thực hiện trước khi lời gọi trả quyền điều khiển cho đoạn code bên ngoài.
Giải thích theo các mốc thay vì đọc thuộc chữ
Đầu tiên, A được in. Timer được đăng ký, nhưng callback của nó chưa chạy giữa đoạn script hiện tại. Callback .then() cũng được xếp để xử lý sau phần đồng bộ này.
Khi gọi run(), hàm in D, gặp await rồi tạm dừng phần tiếp theo. Dù giá trị đang await là null, đoạn sau await vẫn được tiếp tục theo cơ chế bất đồng bộ. Bên ngoài hàm tiếp tục in F.
Tiếp theo là các công việc microtask đã được xếp trong ví dụ. Callback in C được đăng ký trước phần tiếp tục in E, nên C xuất hiện trước E. Callback timer in B thuộc một chặng xử lý khác và đến sau các microtask đó trong tình huống này.
Cách trình bày tốt là nói rõ bạn đang theo dõi phần đồng bộ, thứ tự xếp microtask và callback timer. Không cần gán cho mỗi dòng một số mili giây, vì đoạn code không cung cấp một phép đo thời gian thực tế nào.
Câu hỏi tiếp: thêm microtask từ microtask
Promise.resolve().then(() => {
console.log('first');
queueMicrotask(() => console.log('nested'));
});
Promise.resolve().then(() => console.log('second'));
console.log('sync');
Trong ví dụ độc lập, thứ tự dự kiến là sync, first, second, nested. Khi callback đầu tiên chạy, công việc second đã nằm trong hàng đợi; nested được thêm sau nó. Đừng hiểu việc “đang ở trong một microtask” là mọi microtask mới phải chạy ngay bên trong callback hiện tại.
Bạn có thể dùng sơ đồ hàng đợi trên giấy để giải thích. Việc đặt tên từng công việc giúp dễ kiểm tra hơn một lời giải chỉ nói chung rằng Promise nhanh hơn timer. Không có phép so sánh tốc độ nào cần thiết ở đây; vấn đề là quy tắc thực thi và thứ tự đăng ký.
Những phát biểu dễ mất điểm
“setTimeout(..., 0) chạy ngay” nhầm điều kiện đủ thời gian với quyền chen ngang đoạn code đang chạy. “await chặn cả chương trình” lại nhầm việc tạm dừng một hàm với việc khóa main thread. Tuy nhiên, một vòng lặp đồng bộ dài trước hoặc sau await vẫn có thể chặn thread đang thực thi.
Một vòng lặp liên tục tạo microtask có thể làm công việc khác bị trì hoãn. Vì vậy, thay mọi tác vụ nặng bằng queueMicrotask() không biến chúng thành công việc chạy ở thread khác. Nếu cần xử lý tính toán dài, phải chọn chiến lược chia nhỏ hoặc cơ chế xử lý nền phù hợp với môi trường.
Đừng áp dụng máy móc các ví dụ có process.nextTick() hoặc setImmediate() cho trình duyệt. Tương tự, bài tập trên không đủ để kết luận một lần render React hay một frame React Native sẽ xuất hiện chính xác ở đâu. Khi câu hỏi có API của host, hãy hỏi rõ runtime và cách chạy script/module.
Cách luyện trong mười phút
Lưu ví dụ đầu thành event-loop.cjs nếu muốn thử script CommonJS độc lập bằng Node. Chạy node --version rồi node event-loop.cjs. So đầu ra với dự đoán, sau đó di chuyển một lời gọi .then() và giải thích sự thay đổi trước khi chạy lại.
Tiếp tục thay await null bằng await Promise.resolve() rồi thử một Promise được giải quyết bằng timer. Mỗi lần chỉ thay một yếu tố. Ghi lại đâu là điều bạn suy từ quy tắc ngôn ngữ và đâu là điều quan sát ở runtime đang dùng.
Trong phỏng vấn, một câu trả lời biết chỉ ra giới hạn môi trường thường hữu ích hơn việc khẳng định mọi đoạn async đều hoạt động giống nhau. Hãy dùng ví dụ nhỏ để làm rõ lý do, rồi liên hệ với lỗi thực tế như cập nhật UI từ một kết quả cũ hoặc thực hiện công việc nặng trên thread giao diện.
Kết luận
Hãy giải thích trình tự đăng ký và tiếp tục thực thi, thay vì ghi nhớ thứ tự chữ cái. Khi đã tách được ngôn ngữ JavaScript khỏi API của runtime, bạn sẽ dễ xử lý cả bài tập phỏng vấn lẫn lỗi bất đồng bộ trong ứng dụng.




