JavaScript là ngôn ngữ đơn luồng, nghĩa là chỉ có một luồng thực thi chính. Mọi lệnh đều được xử lý tuần tự. Tuy nhiên, nếu một thao tác mất nhiều thời gian như setTimeout hay gọi Ajax, nó sẽ làm tắc nghẽn các tác vụ tiếp theo, khiến chương trình chạy chậm. Để khắc phục điều này, JavaScript có một cơ chế xử lý đặc biệt với nhiều "kênh" khác nhau.
Đầu tiên là call stack (ngăn xếp lời gọi), nơi xử lý các tác vụ ngắn và nhanh. Những tác vụ tốn thời gian hơn được đưa vào task queue (hàng đợi tác vụ). Task queue lại chia thành macro-task và micro-task:
- Micro-task: chứa các tác vụ như
promise.then,async/await. - Macro-task: chứa các tác vụ như
setTimeout, Ajax, sự kiện click.
Khi call stack rỗng, JavaScript sẽ kiểm tra micro-task queue trước, xử lý hết chúng, sau đó mới chuyển sang macro-task queue.
Về cấu trúc dữ liệu: Stack (ngăn xếp) hoạt động theo nguyên tắc LIFO (Last In, First Out) – vào sau ra trước. Queue (hàng đợi) hoạt động theo FIFO (First In, First Out) – vào trước ra trước.
Hãy xem xét một ví dụ đơn giản để hiểu thứ tự thực thi:
new Promise(resolve => {
console.log('promise');
resolve(5);
}).then(value => {
console.log('then callback', value);
});
function func1() {
console.log('func1');
}
setTimeout(() => {
console.log('setTimeout');
});
func1();
Phân tích:
- Lời gọi
new Promise– hàm executor chạy ngay lập tức, in rapromise. .then()được đưa vào micro-task queue.func1chưa được gọi, nên chưa vào stack.setTimeoutđưa callback vào macro-task queue.- Gọi
func1()– hàm chạy và in rafunc1.
Sau khi stack rỗng: micro-task queue in then callback 5, sau đó macro-task in setTimeout.
Kết quả: promise → func1 → then callback 5 → setTimeout.
Ví dụ phức tạp hơn:
setTimeout(function () {
console.log("set1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("pr1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("set2");
});
console.log(2);
queueMicrotask(() => {
console.log("queueMicrotask1");
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
Phân tích từng bước:
setTimeout("set1")→ macro-task queue.Promise(pr1)chạy executor, inpr1..then(then1)→ micro-task queue.setTimeout("set2")→ macro-task queue (sau set1).- In
2(từ call stack). queueMicrotask→ micro-task queue (tương tựpromise.then).Promise(then3)tương tự,.then(then3)→ micro-task queue.
Sau khi stack rỗng, thực thi tuần tự micro-task queue: then1 → queueMicrotask1 → then3.
Tiếp đến macro-task: Lấy set1, chạy callback: in set1 → Promise(then inside set1) → .then(then2) và .then(then4) (đây lại là micro-task mới).
Kết quả cuối cùng:
pr1
2
then1
queueMicrotask1
then3
set1
then2
then4
set2
Ví dụ cuối với async/await:
async function async1 () {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2 () {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
Phân tích:
- In
script start. setTimeout→ macro-task queue.- Gọi
async1(): inasync1 start, gọiasync2()(inasync2), gặpawait→ đưaasync1 endvào micro-task queue. Promise(promise1)chạy executor, inpromise1,.then(promise2)→ micro-task queue.- In
script end.
Sau stack: micro-task chạy async1 end rồi promise2. Cuối cùng macro-task in setTimeout.
Kết quả:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Tổng kết các quy tắc quan trọng:
- Hàm executor của Promise chạy ngay lập tức trong call stack, còn
.then()được đưa vào micro-task queue. - Trong hàm
async, code trướcawaitchạy đồng bộ; code ngay sauawaitđược chuyển thành micro-task. - Call stack rỗng → kiểm tra micro-task queue (xử lý hết) → sau đó mới lấy macro-task.