This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
JavaScript là một ngôn ngữ lập trình đơn luồng (Single-threaded). Tuy nhiên, trình duyệt web vẫn có thể xử lý mượt mà hàng trăm yêu cầu mạng (Network I/O), sự kiện người dùng và hoạt ảnh đồng thời mà không hề làm treo giao diện (UI Blocking). Chìa khóa của sức mạnh này nằm ở cơ chế lặp dữ liệu Iterators, hàm tạm dừng Generator, và con tim xử lý bất đồng bộ Event Loop.
1. Giao Thức Iterators & Generator — Cơ Chế Tạm Dừng Hàm Đặc Biệt
Trong JavaScript, các cấu trúc dữ liệu quen thuộc như Array, String, Map, Set có thể được duyệt một
cách tự nhiên thông qua vòng lặp for...of hay spread operator [...]. Điều
này đạt được nhờ chúng tuân thủ Iterable Protocol (Giao thức lặp):
-
Một đối tượng được coi là Iterable nếu nó định nghĩa thuộc tính đặc biệt mang khóa
[Symbol.iterator]. Phương thức này phải trả về một đối tượng Iterator. -
Iterator bắt buộc phải sở hữu phương thức
next(), mỗi lần gọi sẽ trả về một đối tượng chứa trạng thái duyệt:{ value: any, done: boolean }. Khi duyệt hết,donesẽ mang giá trịtrue.
Hàm Generator (function*) và từ khóa yield
Việc tự viết một Iterator thủ công khá phức tạp vì bạn phải tự quản lý trạng thái index cục bộ. Generator ra đời để đơn giản hóa quá trình này:
- Khi gọi một hàm Generator, V8 Engine không thực thi các dòng lệnh của nó ngay lập tức. Thay vào đó, nó trả về một đối tượng Generator đặc biệt (vừa là Iterable vừa là Iterator).
-
Mỗi lần bạn gọi phương thức
next(), hàm sẽ chạy cho tới khi gặp từ khóayield. Tại đây, hàm sẽ tạm dừng thực thi hoàn toàn (Suspend), trả về giá trị phía sau yield và đóng băng toàn bộ ngữ cảnh hoạt động (biến cục bộ, vị trí con trỏ lệnh). -
Lần gọi
next()kế tiếp sẽ giải phóng trạng thái đóng băng và khôi phục hàm chạy tiếp ngay tại vị trí tạm dừng trước đó. Đây là nền tảng của lập trình bất đồng bộ dạng không chặn (Non-blocking).
// Generator sinh chuỗi Fibonacci vô hạn cực kì tiết kiệm bộ nhớ
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
2. Bản Chất Bất Đồng Bộ: Mổ Xẻ Vòng Đời Của Event Loop
Vì JavaScript là ngôn ngữ đơn luồng, nó chỉ có duy nhất một Call Stack để thực thi mã nguồn. Nếu bạn chạy một tác vụ nặng đồng bộ (như tính toán ma trận lớn, đợi phản hồi mạng), trình duyệt sẽ bị "đơ" hoàn toàn (Blocking).
Để giải quyết, JS Engine đẩy các tác vụ bất đồng bộ sang cho Web APIs (môi trường C++ đa luồng của trình duyệt) hoặc C++ Thread Pool (libuv) của Node.js xử lý ngầm. Khi các tác vụ này hoàn thành, các hàm phản hồi (Callbacks) sẽ được đưa vào hàng đợi để chờ thực thi. Cơ chế giám sát và điều phối này được gọi là Event Loop.
Sơ đồ kiến trúc Event Loop:
┌────────────────────────────────────────────────────────┐
│ JavaScript Engine │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Call Stack │ │ Memory Heap │ │
│ └──────┬───────┘ └─────────────────┘ │
└─────────┼──────────────────────────────────────────────┘
│ Đẩy tác vụ ngầm (e.g. setTimeout, fetch)
▼
┌─────────────────┐
│ Web APIs ├─┐
└─────────────────┘ │ callback hoàn thành
▼
┌────────────────────────────────────────────────────────┐
│ Event Loop Queue │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Microtask Queue (Promises, queueMicrotask) │ │
│ └──────────────────────┬───────────────────────────┘ │
│ ▼ Ưu tiên cao (Dọn sạch) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Macrotask Queue (setTimeout, I/O) │ │
│ └──────────────────────┬───────────────────────────┘ │
└─────────────────────────┼──────────────────────────────┘
│ Event Loop kéo lên Stack khi Stack trống
▼
Quy tắc ưu tiên của 2 loại Hàng Đợi (Queues):
Khi Call Stack trống hoàn toàn, Event Loop sẽ bắt đầu một vòng lặp (Tick) điều phối dữ liệu:
-
Microtask Queue (Ưu tiên cao nhất): Chứa callback của
Promise.then(),queueMicrotask(),MutationObserver. Quy tắc: Event Loop bắt buộc phải dọn sạch hoàn toàn mọi tác vụ trong Microtask Queue (kể cả các microtask mới được chèn vào trong quá trình chạy) cho tới khi hàng đợi này trống rỗng. -
Macrotask Queue (Task Queue - Ưu tiên thấp hơn): Chứa callback của
setTimeout(),setInterval(), sự kiện click của người dùng, I/O mạng. Quy tắc: Event Loop chỉ lấy duy nhất 1 tác vụ ở đầu Macrotask Queue đẩy lên Call Stack để chạy, sau đó quay lại kiểm tra và dọn sạch Microtask Queue trước khi lấy tác vụ macrotask tiếp theo.
Cơ chế cập nhật giao diện (UI Rendering) của trình duyệt sẽ được ưu tiên chạy xen kẽ sau khi Microtask Queue đã được dọn sạch, giúp giao diện luôn mượt mà.
3. Sự Tiến Hóa Của Xử Lý Bất Đồng Bộ: Promises & Async/Await
Để thoát khỏi thảm họa lồng hàm Callback Hell, ES6 giới thiệu Promise biểu diễn một giá trị bất đồng bộ ở 3 trạng thái: Pending (Đang chờ), Fulfilled (Thành công) và Rejected (Thất bại). Trạng thái của một Promise một khi đã được giải quyết (Settled) thì không thể thay đổi, đảm bảo tính nhất quán.
ES8 mang lại từ khóa async và await. Đây thực chất là
Syntactic Sugar được compiler dịch ngược thành sự kết hợp giữa
Generators và Promises:
- Hàm đánh dấu
asyncluôn tự động trả về một Promise. -
Từ khóa
awaitđóng vai trò tương tự như lệnhyield, tạm dừng hàm async và đẩy phần code phía sau vào hàng đợi Microtask Queue dưới dạng callback của Promise.
Error Handling trong Async & Unhandled Rejection
Xử lý lỗi bất đồng bộ có một cạm bẫy kinh điển: try/catch đồng bộ
không thể bắt được lỗi ném ra bên trong một callback bất đồng bộ (như
setTimeout hay event listener) vì hàm chứa try/catch đã kết thúc và được giải phóng khỏi
Call Stack trước khi callback đó thực thi.
// ❌ SAI: try/catch không bắt được lỗi trong callback bất đồng bộ
try {
setTimeout(() => {
throw new Error("Lỗi ngầm!"); // Bị bỏ qua bởi try/catch bên ngoài!
}, 100);
} catch(e) {
console.log("Không bao giờ chạy vào đây!");
}
// ✅ ĐÚNG: dùng try/catch bao quanh lệnh await trong hàm async
async function task() {
try {
const data = await fetchUserData(-1); // Promise bị reject
} catch (e) {
console.log("Bắt được lỗi:", e.message); // Xử lý lỗi an toàn ở đây
}
}
// Lắng nghe các Promise bị lỗi mà không có .catch() (Unhandled Rejection)
globalThis.onunhandledrejection = (event) => {
console.warn("⚠️ Promise bị lỗi chưa xử lý:", event.reason.message);
};
Async Iterables & Async Generators
Khi bạn cần duyệt một chuỗi dữ liệu bất đồng bộ (ví dụ: các trang kết quả từ API kết nối chậm, hay
stream dữ liệu từ một file lớn), bạn không thể dùng Iterator thông thường. Bạn cần
Async Iterator Protocol và vòng lặp for await...of:
-
Async Iterator: Phương thức
next()trả về một Promise phân giải thành{ value, done }. -
Async Generator: Được khai báo bằng
async function*, cho phép sử dụng cảawaitđể chờ tác vụ vàyieldđể sinh dữ liệu.
4. Code Thực Hành Nâng Cao: iterators_generators.js
Hãy chạy thử chương trình dưới đây để nhìn thấy chính xác thứ tự thực thi của các tác vụ đồng bộ, Microtask và Macrotask được phân luồng bởi Event Loop:
/**
* Bài 4: Iterators, Generators & Lập Trình Bất Đồng Bộ (Event Loop Internals)
* Giáo trình tự học JavaScript - js-tools.org
*/
console.log("=== 1. Custom Iterators (Tự định nghĩa giao thức duyệt) ===");
// Tạo một đối tượng Range số nguyên cho phép duyệt bằng vòng lặp for...of
const numberRange = {
from: 1,
to: 5,
// Định nghĩa phương thức Symbol.iterator
[Symbol.iterator]() {
let current = this.from;
let last = this.to;
// Giao thức iterator phải trả về một đối tượng có phương thức next()
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
console.log("Duyệt qua đối tượng range số nguyên tự định nghĩa:");
for (let num of numberRange) {
console.log(num); // In từ 1 đến 5
}
console.log("\n=== 2. Generators (Hàm sinh tạm dừng bằng yield) ===");
// Generator function tạo chuỗi số Fibonacci vô hạn
function* fibonacciSequence() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fibGen = fibonacciSequence();
console.log("5 số Fibonacci đầu tiên sinh ra từ Generator:");
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 2
console.log(fibGen.next().value); // 3
console.log(fibGen.next().value); // 5
console.log("\n=== 3. Lập trình bất đồng bộ: Promises và Async/Await ===");
// Giả lập hàm fetch dữ liệu bất đồng bộ từ API
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, username: `user_${userId}`, active: true });
} else {
reject(new Error("Mã người dùng không hợp lệ"));
}
}, 100);
});
}
// Gọi bằng Promise Chain
fetchUserData(10)
.then(user => console.log("Promise.then: Đã tải thông tin", user.username))
.catch(err => console.error("Lỗi:", err.message));
// Gọi bằng Async/Await để viết code bất đồng bộ trông giống như đồng bộ
async function runAsyncDemo() {
console.log("Khởi động async call...");
try {
const user = await fetchUserData(42);
console.log("Async/Await: Tải thành công user:", user);
} catch (err) {
console.error("Lỗi Async/Await:", err.message);
}
}
runAsyncDemo();
console.log("\n=== 4. Async Generators và Vòng lặp 'for await...of' ===");
// Giả lập một Generator sinh dữ liệu bất đồng bộ (ví dụ: kéo trang API ngắt quãng)
async function* fetchPages(maxPages) {
for (let page = 1; page <= maxPages; page++) {
// Chờ 50ms trước khi tải trang tiếp theo
await new Promise(resolve => setTimeout(resolve, 50));
yield `Dữ liệu trang số ${page}`;
}
}
async function readAllPages() {
const pagesGen = fetchPages(3);
console.log("Bắt đầu duyệt trang bất đồng bộ:");
for await (const pageData of pagesGen) {
console.log("Nhận được:", pageData);
}
console.log("Hoàn thành duyệt toàn bộ các trang.");
}
readAllPages();
console.log("\n=== 5. Trực quan hóa thứ tự chạy của Event Loop ===");
// Macrotask Queue
setTimeout(() => {
console.log("[Macrotask] Callback của setTimeout(0ms) thực thi");
}, 0);
// Microtask Queue
Promise.resolve().then(() => {
console.log("[Microtask 1] Promise.then callback thực thi");
});
queueMicrotask(() => {
console.log("[Microtask 2] queueMicrotask callback thực thi");
});
// Đồng bộ (Synchronous)
console.log("[Sync] Lệnh đồng bộ cuối cùng của file code");
5. Playground: Trực Quan Hóa Thứ Tự Event Loop
Chạy đoạn code dưới đây và đoán thứ tự output trước khi bấm Run. Đây là bài tập tư duy cực kỳ quan trọng:
6. Promise Combinators — all, race, allSettled, any
ES2020-2021 bổ sung đầy đủ 4 combinators để xử lý nhiều Promise song song:
const delay = (ms, val) => new Promise(res => setTimeout(() => res(val), ms));
const fail = (ms, msg) => new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms));
// 1. Promise.all: chờ TẤT CẢ thành công, 1 fail → reject ngay
Promise.all([delay(100, 'A'), delay(200, 'B'), delay(50, 'C')])
.then(results => console.log('all:', results)); // ["A", "B", "C"] sau 200ms
// Nếu 1 reject → toàn bộ reject ngay
// 2. Promise.race: lấy kết quả của Promise nhanh nhất
Promise.race([delay(300, 'slow'), delay(100, 'fast')])
.then(result => console.log('race:', result)); // "fast" sau 100ms
// 3. Promise.allSettled: chờ TẤT CẢ, không quan tâm thành/bại
Promise.allSettled([delay(100, 'ok'), fail(200, 'oops'), delay(50, 'also ok')])
.then(results => results.forEach(r =>
console.log('allSettled:', r.status, r.value ?? r.reason?.message)
));
// fulfilled ok / rejected oops / fulfilled also ok
// 4. Promise.any: lấy promise thành công đầu tiên (ES2021)
Promise.any([fail(50, 'err1'), delay(100, 'winner'), fail(150, 'err2')])
.then(result => console.log('any:', result)); // "winner" sau 100ms
// Nếu TẤT CẢ reject → AggregateError
// So sánh nhanh:
// all: cần tất cả thành công | race: cần 1 xong sớm nhất
// allSettled: muốn biết kết quả tất cả | any: cần 1 thành công nhanh nhất
7. AbortController — Hủy Fetch & Async Operations
AbortController cho phép hủy bỏ fetch request đang chờ hoặc bất kỳ async operation nào hỗ
trợ AbortSignal. Đây là pattern quan trọng để tránh memory leak trong React components:
// Basic AbortController usage
const controller = new AbortController();
const signal = controller.signal;
// Timeout tự động sau 5 giây
const timeoutId = setTimeout(() => controller.abort('Timeout!'), 5000);
async function fetchWithAbort(url) {
try {
const response = await fetch(url, { signal });
clearTimeout(timeoutId);
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch bị hủy:', signal.reason);
} else {
throw err;
}
}
}
// ES2022: AbortSignal.timeout() — shortcut built-in
const response = await fetch(url, {
signal: AbortSignal.timeout(5000) // Tự động abort sau 5s
});
// Pattern trong React:
// useEffect(() => {
// const controller = new AbortController();
// fetchData(controller.signal).then(setData);
// return () => controller.abort(); // cleanup khi unmount
// }, []);
// Kết hợp nhiều signal:
const c1 = new AbortController();
const c2 = new AbortController();
const combined = AbortSignal.any([c1.signal, c2.signal]); // ES2024
// abort khi c1 hoặc c2 abort
Trắc nghiệm 1: Thứ Tự Event Loop
Thứ tự in ra của đoạn code này là gì?console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() =>
console.log(3)); console.log(4);
Trắc nghiệm 2: Promise Combinator
Bạn cần gọi 5 API song song, muốn biết kết quả của TẤT CẢ kể cả lỗi, không muốn dừng khi có 1 lỗi. Combinator nào phù hợp?
Tải file code thực hành minh họa bài học
File đầy đủ: Custom Iterator, Generator vô hạn, Promise combinators, AbortController, Async Generator và Event Loop order visualization:
Tải về iterators_generators.js
Comments
Bình luận