Bài 8 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Bất đồng bộ
Biên soạn bởi Nguyễn Anh Tuấn
Lập trình bất đồng bộ (asynchronous) trong JavaScript: callback → Promise → async/await, và event loop với microtask/macrotask - vì sao JS không đứng hình.
JavaScript chạy trên một luồng (một call stack): mỗi lúc chỉ làm một việc. Nếu một việc chạy lâu theo kiểu đồng bộ, mọi thứ khác phải chờ - trang web “đứng hình”. Giải pháp: giao tác vụ chậm (mạng, timer, file) cho môi trường lo, và nhận kết quả sau qua callback - không chặn luồng.
đồng bộ (chặn) vs bất đồng bộ (không chặn)
// DONG BO: treo luong cho toi khi xong
console.log("A");
let s = 0;
for (let i = 0; i < 1e9; i++) s += i; // nang -> moi thu phai cho
console.log("B");
// BAT DONG BO: giao viec cham, khong cho
console.log("C");
setTimeout(() => console.log("D (sau)"), 1000); // chay o nen
console.log("E"); // in NGAY, khong doi D - ▸JS đơn luồng: một call stack, mỗi lúc một việc.
- ▸Code đồng bộ chặn luồng tới khi xong → có thể làm “đứng hình”.
- ▸Tác vụ chậm giao cho môi trường + callback khi xong → không chặn.
Callback là hàm bạn truyền vào để được gọi LẠI khi việc hoàn tất. Hợp cho một việc, nhưng lồng nhiều lớp sẽ thành callback hell - khó đọc, khó bắt lỗi.
callback và mớ lồng
function taiDuLieu(xong) { // "xong" la callback
setTimeout(() => xong("du lieu"), 500);
}
taiDuLieu((kq) => console.log("nhan:", kq));
// Long nhieu lop -> "callback hell":
buoc1(() => {
buoc2(() => {
buoc3(() => console.log("xong het"));
});
}); - ▸Callback = hàm gọi-lại khi tác vụ xong.
- ▸Tốt cho một việc; lồng sâu → khó đọc, khó xử lý lỗi (callback hell).
- ▸Promise và async/await sinh ra để làm phẳng mớ lồng này.
Promise là object đại diện cho một kết quả sẽ có trong tương lai. Nó đi từ pending (đang chờ) sang fulfilled (thành công) hoặc rejected (lỗi). Dùng .then/.catch, và nối chuỗi thay cho lồng:
tạo & dùng Promise
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("OK"), 300); // xong -> resolve
});
p.then((kq) => console.log(kq)) // "OK"
.catch((e) => console.log("loi:", e));
// Noi chuoi (thay cho long callback):
taiUser()
.then((u) => taiBaiViet(u.id))
.then((bv) => console.log(bv))
.catch((e) => console.log(e)); - ▸Ba trạng thái: pending → fulfilled (value) hoặc rejected (lỗi).
- ▸.then xử lý thành công; .catch bắt lỗi cho cả chuỗi.
- ▸Mỗi .then trả về một Promise mới → nối chuỗi phẳng, dễ đọc.
async/await là cách viết gọn hơn (syntactic sugar) bọc ngoài Promise. async đánh dấu hàm luôn trả về Promise; await chờ một Promise xong rồi lấy giá trị - đọc tuần tự như code đồng bộ, nhưng vẫn không chặn luồng. Lỗi bắt bằng try/catch:
cùng việc ở Bước 3, viết bằng async/await
async function tai() {
try {
const u = await taiUser(); // "cho" promise xong
const bv = await taiBaiViet(u.id); // tuan tu, de doc
console.log(bv);
} catch (e) {
console.log("loi:", e); // bat moi loi trong chuoi
}
}
tai(); - ▸async: hàm luôn trả về Promise. await: chờ Promise xong (không chặn luồng).
- ▸try/catch bắt lỗi y như code đồng bộ.
- ▸Đọc tuần tự, dễ hơn chuỗi .then dài.
Chạy song song khi có thể
Tất cả ráp lại ở event loop. Khi call stack rảnh, nó lấy việc từ hàng đợi để chạy - nhưng theo thứ tự ưu tiên: đồng bộ trước, rồi vét HẾT microtask (Promise.then), cuối cùng mới tới macrotask (setTimeout). Bấm “Bước” để xem thứ tự in ra:
câu đố kinh điển
console.log("1"); // dong bo
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4"); // dong bo
// In ra: 1, 4, 3, 2 Kết quả khi chạy
1 4 3 2
- ▸Event loop chạy callback từ hàng đợi khi call stack rảnh.
- ▸Thứ tự: đồng bộ → vét hết microtask → một macrotask (rồi lặp lại).
- ▸setTimeout(…, 0) vẫn chạy SAU toàn bộ code đồng bộ và microtask.
Trung thực: đây là mô hình rút gọn
Tiếp theo
Câu hỏi thường gặp
JS chạy MỘT luồng (một call stack). Tác vụ chậm (mạng, timer, đọc file) được giao cho MÔI TRƯỜNG (trình duyệt/Node) xử lý ở nền; khi xong, callback được đưa vào hàng đợi và event loop chạy nó khi call stack rảnh. Nhờ vậy giao diện không “đứng hình”.
Đa luồng & đa tiến trình - khoá JS nâng cao →Là khi lồng nhiều callback vào nhau (mỗi việc chờ việc trước) khiến code thụt sâu, khó đọc và khó xử lý lỗi. Promise (nối chuỗi) và async/await ra đời để “làm phẳng” mớ lồng đó.
Promise là một object đại diện cho kết quả TƯƠNG LAI. Ba trạng thái: pending (đang chờ) → rồi fulfilled (thành công, có value) hoặc rejected (lỗi). Dùng .then cho thành công, .catch cho lỗi; và nối chuỗi nhiều .then.
async đánh dấu một hàm LUÔN trả về Promise. await “tạm dừng” bên trong hàm async cho tới khi Promise xong rồi lấy giá trị - viết như đồng bộ nhưng KHÔNG chặn luồng. Lỗi bắt bằng try/catch như code thường.
Vì callback của setTimeout là một MACROTASK: nó phải đợi toàn bộ code đồng bộ chạy xong (và microtask vét hết) mới tới lượt. “0ms” chỉ nghĩa là “sớm nhất có thể”, không phải “ngay bây giờ”.
microtask (Promise.then, queueMicrotask) được ƯU TIÊN: sau mỗi mẩu code đồng bộ, event loop VÉT HẾT microtask trước khi lấy MỘT macrotask (setTimeout, sự kiện). Vì vậy Promise.then chạy trước setTimeout dù cùng “0ms”.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
JavaScript đơn luồng vẫn xử lý được nhiều việc nhờ điều gì?
- 1
Chặn luồng
Viết một vòng lặp nặng (cộng dồn tới
1e9) rồi log sau nó. Code sau có chạy được trong lúc lặp không?Hoàn thành khi: Không - code đồng bộ chặn luồng tới khi vòng lặp xong mới log.
- 2
Callback
In "trước";
setTimeoutin "sau" sau 1s; in "cuối" ngay. Thứ tự in ra?Hoàn thành khi: "trước", "cuối" in ngay; "sau" in sau ~1s - dù đặt giữa.
- 3
Promise
Tạo
Promise.resolve(42).then(in). Và mộtPromise.reject("x").catch(in lỗi).Hoàn thành khi:
thennhận 42;catchbắt được"x"(không làm sập chương trình). - 4
async/await
Viết hàm
asynccóawaitmộtPromise(vd delay 300ms), in trước & sauawait; bọctry/catch.Hoàn thành khi: Dòng sau
awaitchạy SAU khi promise xong; lỗi rơi vàocatch. - 5
Đoán thứ tự
Dùng công cụ ở Bước 5: trước khi bấm, đoán 1/2/3/4 in ra theo thứ tự nào.
Hoàn thành khi: 1, 4, 3, 2 - đồng bộ → microtask → macrotask.
- 6
micro vs macro
Cùng lúc đặt một
Promise.thenvà mộtsetTimeout(…, 0). Cái nào in trước? Vì sao?Hoàn thành khi:
Promise.then(microtask) trướcsetTimeout(macrotask) - micro được vét trước.