Giải thích về mô hình đơn luồng
JavaScript được thiết kế theo mô hình đơn luồng để đảm bảo tính nhất quán khi thao tác DOM. Nếu cho phép đa luồng, việc đồng bộ hóa tài nguyên DOM sẽ dẫn đến tình trạng race condition. Ví dụ:
document.getElementById("btn").addEventListener("click", () => {
console.log("Xử lý click 1");
});
document.getElementById("btn").addEventListener("click", () => {
console.log("Xử lý click 2");
});
Cả hai sự kiện click đều được xếp vào hàng đợi và xử lý tuần tự trên cùng một luồng chính.
Tại sao cần bất đồng bộ?
Việc thực thi đồng bộ có thể gây treo giao diện. Xét ví dụ:
function heavyTask() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
return sum;
}
console.log("Bắt đầu");
heavyTask();
console.log("Kết thúc");
Kết quả: "Bắt đầu" được in ngay lập tức, sau đó là thời gian chờ dài để tính toán, cuối cùng mới in "Kết thúc".
Cơ chế thực thi bất đồng bộ
- Call Stack: Ngăn xếp thực thi chứa các hàm đang chạy
- Callback Queue: Hàng đợi chứa các hàm bất đồng bộ đã sẵn sàng
- Event Loop: Vòng lặp kiểm tra và chuyển hàm từ hàng đợi vào ngăn xếp
Phân loại nhiệm vụ
JavaScript phân biệt hai loại nhiệm vụ:
| Loại | Ví dụ |
|---|---|
| Microtask | Promise.then, MutationObserver |
| Macrotask | setTimeout, setInterval, DOM events |
Thứ tự ưu tiên thực thi
Xét đoạn mã sau:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
// Kết quả: Start → End → Promise → Timeout
Giải thích: Microtask (Promise) luôn được ưu tiên hơn Macrotask (setTimeout) trong cùng một vòng lặp sự kiện.
Ứng dụng thực tế trong framework
Ví dụ trong Vue.js:
this.items.push("new item");
this.$nextTick(() => {
// Mã chạy sau khi DOM cập nhật
console.log(document.getElementById("list").innerHTML);
});
Hàm $nextTick tận dụng hàng đợi Microtask để đảm bảo DOM đã được cập nhật trước khi thực thi callback.