Lập trình & Kỹ thuậtĐộ khó: N3 / Nhập môn⏱ 7 phút đọc

Người mới học JavaScript thử tìm hiểu bản chất của “Bất đồng bộ (Asynchronous)” (JS初心者が『非同期』を理解してみた)

概Điểm nhấn trọng tâm của bài viết (Overview)

Giải thích trực quan sự khác biệt giữa Xử lý đồng bộ (同期処理) và Xử lý bất đồng bộ (非同期処理) trong JavaScript.

Người mới học JavaScript thử tìm hiểu bản chất của “Bất đồng bộ (Asynchronous)” (JS初心者が『非同期』を理解してみた)

✍️ Tác giả gốc: koxrtx (株式会社シンシア) trên diễn đàn công nghệ Qiita Nhật Bản
🔗 Tiêu đề gốc có Furigana: JS初心者しょしんしゃが『非同期ひどうき』を理解りかいしてみた
🌐 Liên kết bài gốc: JS初心者が『非同期』を理解してみた

📌 Điểm nhấn cốt lõi (Key Takeaways)

  • Giải thích trực quan sự khác biệt giữa Xử lý đồng bộ (同期処理) và Xử lý bất đồng bộ (非同期処理) trong JavaScript.
  • Minh họa bằng ví dụ thực tế với fetch, setTimeout (5 giây và 0 giây), Promise (3 trạng thái: pending, fulfilled, rejected) và async/await.
  • Chỉ ra nguyên nhân gây đơ màn hình (UI freeze) khi dùng đồng bộ cho tác vụ nặng và lỗi undefined kinh điển khi chưa đợi kết quả bất đồng bộ.

1. Mở đầu (1. はじめに)

Trước đây tôi chỉ hiểu lơ mơ rằng bất đồng bộ là thứ gì đó chạy ngầm phía sau để xử lý êm xuôi các tác vụ nặng, chứ chưa thực sự hiểu rõ bất đồng bộ đang làm gì. Dù có được hỏi “Tại sao phải viết bằng bất đồng bộ?”, tôi cũng chỉ trả lời được đại khái là “Nếu không bất đồng bộ thì màn hình sẽ không hiển thị cho đến khi xử lý xong và bị đơ nặng”. Vì lý do tại sao bắt buộc phải dùng bất đồng bộ vẫn còn mơ hồ, nên khi thực tế viết code tôi rất hay phân vân “Chỗ này dùng bất đồng bộ có ổn không nhỉ?”. Vì vậy trong bài viết này, tôi đặt mục tiêu là hiểu rõ xử lý bất đồng bộ thực chất đang làm những gì.

2. Xử lý đồng bộ (Synchronous Processing) (2. 同期処理どうきしょり)

console.log('1');
heavyWork();      // これが終わるまで待つ
console.log('2'); // heavyWork が終わってから実行される

📝 Trước khi tìm hiểu xử lý bất đồng bộ, tôi sẽ giải thích về xử lý đồng bộ. Xử lý đồng bộ là: Cách thức thực thi lần lượt từng lệnh từ trên xuống dưới, và không chuyển sang dòng tiếp theo cho đến khi xử lý phía trước hoàn tất.

Chuyện gì xảy ra nếu gọi một tác vụ tốn thời gian bằng xử lý đồng bộ? Việc truyền thông lấy dữ liệu từ máy chủ (server) nhanh hay chậm còn tùy thuộc vào tình trạng máy chủ và tốc độ đường truyền nên không biết khi nào mới xong. Do đó nếu viết đồng bộ, chương trình không thể sang dòng kế tiếp cho tới khi dữ liệu trả về, mạng càng chậm thì màn hình càng bị đơ cứng lâu. Trong khoảng thời gian đó sẽ xảy ra các hiện tượng như: “Bấm nút không phản hồi”, “Không cuộn trang được”, “Hiển thị màn hình không cập nhật”, “Hiệu ứng chuyển động bị đứng”, khiến người dùng nhìn vào cảm giác như trang web đã bị treo cứng. Tài liệu chính thức (MDN) cũng nêu rõ: “Tại một thời điểm chỉ có thể làm một việc duy nhất. Vì thế, từ lúc gọi một xử lý đồng bộ có thời gian thực thi dài cho đến khi nó quay trở lại, trong suốt khoảng thời gian đó không thể làm bất cứ việc gì khác.”

  • Vậy khi nào thì dùng xử lý đồng bộ? Thực tế, hầu hết code chúng ta viết hằng ngày đều được viết bằng xử lý đồng bộ: Tính toán (tính tổng tiền, giá đã gồm thuế); Thao tác mảng (lọc hoặc sắp xếp danh sách bằng map, filter, sort); Xử lý chuỗi (ghép tên, định dạng ngày tháng); Rẽ nhánh điều kiện (dùng if kiểm tra đã đăng nhập chưa); Kiểm tra đầu vào / Validation (xác nhận định dạng email có hợp lệ không).
  • Những xử lý không tốn thời gian như vậy đều được viết bằng xử lý đồng bộ. Do đó, nếu những nội dung hoàn thành chỉ trong chớp mắt mà cũng viết bất đồng bộ một cách bừa bãi thì chẳng những không nhanh hơn mà còn làm code dài dòng, phức tạp hóa và làm tăng nguy cơ phát sinh bug, nên cần phải phân định rõ ràng.

3. Bất đồng bộ là gì? (3. 非同期ひどうきってなに?)

💡 Trích dẫn: Lập trình bất đồng bộ là kỹ thuật cho phép ngay cả khi bắt đầu một tác vụ có khả năng kéo dài, chương trình không phải ngồi đợi cho đến khi tác vụ đó hoàn tất mà vẫn có thể phản hồi các sự kiện khác trong lúc tác vụ kia đang chạy. Khi tác vụ hoàn tất, chương trình sẽ hiển thị kết quả của nó. (Minh họa bằng sơ đồ: sync_vs_async_timeline.png) (Gốc: 非同期ひどうきプログラミングは、長ながく続つづく可能性かのうせいのあるタスクを開始かいししても、そのタスクが完了かんりょうするまで待まつのではなく、そのタスクの実行中じっこうちゅうも他ほかのイベントに応答おうとうできるようにする技術ぎじゅつです。タスクが完了かんりょうすると、プログラムはその結果けっかを表示ひょうじします。(図ずにするとこんな感かんじ: sync_vs_async_timeline.png))

console.log('1');

fetch('/api/data').then(() => {
  console.log('3:データが届いた'); // 通信が終わったら実行される
});

console.log('2'); // 通信の完了を待たずに、すぐ実行される
// .then() の中には、通信が終わったあとにやりたい処理を書きます

📝 Đúng như trích dẫn từ MDN ở chương 2, JavaScript chỉ có thể làm 1 việc tại 1 thời điểm. Đặc điểm này được gọi là Đơn luồng (Single-threaded): luồng xử lý chỉ có duy nhất 1 sợi. 📝 Vì vậy, nếu viết các tác vụ tốn thời gian như giao tiếp với server bằng xử lý đồng bộ thì phải đợi đến khi xử lý xong, trong lúc đó không thể làm bất kỳ việc gì khác (như phản hồi nút bấm hay cập nhật màn hình) khiến màn hình bị đơ. Do đó, sau khi phát lệnh xong thì không chờ kết quả mà đi tiếp sang xử lý kế tiếp, rồi nhận thông báo hoặc kết quả vào đúng thời điểm xử lý kia hoàn tất — đó chính là xử lý bất đồng bộ. 📝 Kết quả thực thi sẽ theo thứ tự 1 → 2 → 3; trong lúc chờ truyền thông thì JavaScript vẫn không dừng lại nên màn hình không bị đơ.

4. Thử chạy thực tế (4. 実際じっさいに動うごかしてみた)

console.log('1');
setTimeout(() => {
  console.log('2');
}, 5 * 1000); // 5秒後に実行
console.log('3');

📝 Ở chương 3 tôi đã đưa ra ví dụ về giao tiếp mạng, nhưng giao tiếp mạng thì không thử được nếu không có server. Vì thế, tôi sẽ dùng hàm setTimeout có thể chạy thử ngay trên trình duyệt. setTimeout: Hàm thực thi xử lý sau khi thời gian chỉ định đã trôi qua (Ảnh quay màn hình: số 2 đã xuất hiện cuối cùng). 📝 Tại sao lại như vậy: 1. console.log(‘1’) được thực thi → 2. Nhờ trình duyệt thực hiện console.log(‘2’) sau 5 giây bằng setTimeout → 3. Không đợi 5 giây mà chuyển ngay sang console.log(‘3’) tiếp theo → 4. Khi đủ 5 giây, console.log(‘2’) mới được thực thi. Nhân tiện, phần chạy ngầm phía sau trong xử lý bất đồng bộ chính là trình duyệt (Web APIs), vì khó hình dung nên tôi đã vẽ thành sơ đồ (async_setTimeout_flow.png).

console.log('1');
setTimeout(() => {
  console.log('2');
}, 0); // 0秒後に実行
console.log('3');

// --- バグの一例 ---
let data;
fetch('/api/data')
  .then((res) => res.json())
  .then((json) => {
    data = json; // 通信が終わったら、ここで値が入る
  });
console.log(data); // 通信を待たずに先に実行されるのでundefined

📝 Nếu đặt thành 0 giây thì sao? Nhân tiện, nếu đặt thời gian chờ là 0 giây thì chuyện gì sẽ xảy ra? Dù có đặt 0 giây thì số 2 vẫn được xử lý cuối cùng. Tác vụ truyền vào setTimeout dù thời gian là 0 giây vẫn được xử lý như một tác vụ bất đồng bộ. Vì vậy, phải sau khi toàn bộ các xử lý đồng bộ đang chạy (cho đến hết console.log(‘3’)) kết thúc hoàn toàn thì nó mới được thực thi. 📝 Vì vậy hãy cẩn thận: nếu viết bất đồng bộ mà dùng giá trị ngay bên ngoài, sẽ phát sinh bug dữ liệu hiển thị trống rỗng (undefined) dù đã gọi lấy dữ liệu. Cách sửa là: “Viết xử lý sử dụng kết quả vào bên trong hàm bất đồng bộ” hoặc “Dùng await để đợi cho đến khi nhận được kết quả”. Tôi sẽ giới thiệu về await ở chương tiếp theo.

5. Về Promise và async/await (5. Promise と async/await について)

状態 (Trạng thái) 意味 (Ý nghĩa)
待機(pending) まだ結果が出ていない (Chưa có kết quả)
履行(fulfilled) 成功した → .then() が呼ばれる (Thành công → gọi .then())
拒否(rejected) 失敗した → .catch() が呼ばれる (Thất bại → gọi .catch())
async function getData() {
  const response = await fetch('/api/data'); // 結果が出るまでここで待つ
  console.log('データが届いた');
}

// --- 0.5秒ごとにカウントを表示しながら await で5秒待つ比較実験 ---
let count = 0;
const timer = setInterval(() => {
  count++;
  console.log(`カウント ${count}`);
  if (count >= 14) clearInterval(timer);
}, 500);

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getData() {
  console.log('2:待機を開始');
  await wait(5 * 1000);
  console.log('4:5秒たった');
}

console.log('1');
getData();
console.log('3');
// 実行結果: 1 -> 2:待機を開始 -> 3 (5秒を待たずにすぐ表示される) -> カウント... -> 4:5秒たった

// --- 同期処理で書いた場合 ---
function waitSync(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {}
}
function getDataSync() {
  console.log('2:待機を開始');
  waitSync(5 * 1000);
  console.log('4:5秒たった');
}
console.log('1');
getDataSync();
console.log('3');
// 結果は 1 → 2 →(5秒固まる)→ 4 → 3 です。

📝 async/await là gì: Nếu cứ nối chuỗi .then() liên tiếp thì code rất dễ trở nên khó đọc. Lúc đó ta dùng async/await. “Từ khóa async giúp code Promise bất đồng bộ hoạt động đơn giản hơn… Nhờ vậy, dù đang dùng hàm bất đồng bộ nhưng bạn có thể viết code thoạt nhìn giống hệt như code đồng bộ.” async: Đặt trước hàm để biến hàm đó thành hàm bất đồng bộ. await: Đặt trước Promise để đợi ngay tại chỗ đó cho đến khi có kết quả và lấy giá trị bên trong ra. Không cần dùng .then() mà vẫn đọc tuần tự từ trên xuống giống như xử lý đồng bộ. 📝 Nhưng khi nghe nói dùng await để “đợi”, tôi thắc mắc liệu nó có làm đơ màn hình giống xử lý đồng bộ không nên đã kiểm chứng thử. Tôi so sánh trường hợp vừa in bộ đếm mỗi 0.5 giây vừa đợi 5 giây bằng await với trường hợp đợi 5 giây bằng đồng bộ. Nếu trong lúc đợi bộ đếm vẫn chạy tiếp nghĩa là không bị dừng, còn bộ đếm đứng im nghĩa là bị đơ. Với await, sau dòng “2: Bắt đầu chờ”, trong suốt 5 giây chờ thì số 3 và bộ đếm vẫn tiếp tục hiển thị. Đó là vì thứ tạm dừng bởi await chỉ nằm bên trong hàm getData, còn bản thân JavaScript vẫn chạy tiếp. Ngược lại, khi viết bằng đồng bộ, kết quả là 1 → 2 → (đơ 5 giây) → 4 → 3. Sau dòng “2: Bắt đầu chờ”, suốt 5 giây không có gì hiển thị và bộ đếm cũng dừng hẳn, vì JavaScript phải dồn toàn lực vào hàm waitSync suốt 5 giây nên không làm được việc gì khác.

6. Rốt cuộc nên viết bằng Đồng bộ hay Bất đồng bộ? / 7. Tổng kết / Tài liệu tham khảo (6. 結局けっきょく、同期どうきと非同期ひどうきどっちで書かけばいいの? / 7. まとめ / 参考文献さんこうぶんけん)

  • Rốt cuộc nên viết bằng đồng bộ hay bất đồng bộ? — Các xử lý hoàn thành ngay lập tức như tính toán, thao tác mảng, rẽ nhánh điều kiện: Xử lý đồng bộ / Các xử lý có thời gian chờ cho đến khi kết quả trả về như giao tiếp mạng: Xử lý bất đồng bộ.
  • Tổng kết — 1. JavaScript chỉ làm được 1 việc tại 1 thời điểm (Đơn luồng / Single-thread) / 2. Nếu viết các xử lý tốn thời gian như giao tiếp mạng bằng đồng bộ thì trong lúc đó màn hình sẽ bị đơ / 3. Xử lý bất đồng bộ là cơ chế giao phó tác vụ tốn thời gian cho trình duyệt thực hiện để chương trình không bị dừng lại trong lúc chờ đợi.
  • Tài liệu tham khảo: Các trang tài liệu MDN về Async JS và Promise. | Công ty Cổ phần Xincere (株式会社シンシア): Tại Xincere, công ty tuyển dụng và làm việc cùng các kỹ sư chưa có kinh nghiệm thực tế hoặc thực tập sinh kỹ sư (hằng năm có khoảng 100 ứng viên chưa có kinh nghiệm thực tế ứng tuyển và tham gia phỏng vấn kỹ thuật).

📚 Bảng thuật ngữ IT & Hán Việt trong bài (重要IT用語集)

Thuật ngữ (Kanji) Furigana / Romaji Âm Hán Việt Ý nghĩa tiếng Việt Cấp độ
非同期ひどうき ひどうき PHI ĐỒNG KỲ Bất đồng bộ (Asynchronous) Technical
処理しょり しょり XỬ LÝ Xử lý, tác vụ N3
ふわっとふわっと ふわっと - Mơ hồ, chung chung (nghĩa bóng); nhẹ nhàng/bồng bềnh (nghĩa đen) N2
同期処理どうきしょり どうきしょり ĐỒNG KỲ XỬ LÝ Xử lý đồng bộ (Synchronous processing) Technical
実行じっこう じっこう THỰC HÀNH Thực thi, chạy (code/chương trình) N3
呼び出すよびだす よびだす HÔ XUẤT Gọi (hàm / API / xử lý) N3
固まるかたまる かたまる CỐ Bị đơ, bị treo cứng (màn hình/ứng dụng) N3
復帰ふっき ふっき PHỤC QUY Quay trở lại, trả quyền điều khiển về (Return) N2
配列はいれつ はいれつ BÀI LIỆT Mảng (Array) Technical
文字列もじれつ もじれつ VĂN TỰ LIỆT Chuỗi ký tự (String) Technical
条件分岐じょうけんぶんき じょうけんぶんき ĐIỀU KIỆN PHÂN KỲ Rẽ nhánh có điều kiện (Conditional branching: if/else, switch) Technical
むやみやたらにむやみやたらに むやみやたらに - Một cách bừa bãi, vô tội vạ, thiếu suy nghĩ N1
切り分けきりわけ きりわけ THIẾT PHÂN Sự phân định, bóc tách, khoanh vùng (vấn đề/trách nhiệm) Technical
応答おうとう おうとう ỨNG ĐÁP Phản hồi, đáp trả (Respond) N2
シングルスレッドしんぐるすれっど しんぐるすれっど - Đơn luồng (Single thread) Technical
関数かんすう かんすう HÀM SỐ Hàm (Function) Technical
方式ほうしき ほうしき PHƯƠNG THỨC Phương thức, cơ chế N2
待機たいき たいき ĐÃI CƠ Đang chờ (Pending / Standby) N2
履行りこう りこう LÝ HÀNH Thực hiện hoàn tất, hoàn thành cam kết (Fulfilled) N1
拒否きょひ きょひ CỰ PHỦ Từ chối, thất bại (Rejected) N2
一見いっけん いっけん NHẤT KIẾN Thoạt nhìn, nhìn qua N2
かかりきりかかりきり かかりきり - Bận tối mắt vào một việc duy nhất, bị chiếm dụng toàn bộ N1
仕組みしくみ しくみ SĨ TỔ Cơ chế, cấu trúc vận hành N3
実務未経験じつむみけいけん じつむみけいけん THỰC VỤ VỊ KINH NGHIỆM Chưa có kinh nghiệm làm việc thực tế N2

📐 Ngữ pháp tiếng Nhật IT bổ trợ (文法解説)

  • 〜てくれる (N4): Làm gì đó (có ích cho mình)
    • Cấu trúc: V-て + くれる
    • Sắc thái sử dụng: いい感じにしてくれてる = Xử lý êm xuôi giúp cho lập trình viên.
  • 〜くらいしか〜ない (N3): Chỉ có thể… đến mức đó là cùng
    • Cấu trúc: Mệnh đề / Danh từ + くらいしか + V-ない
    • Sắc thái sử dụng: Nhấn mạnh sự hạn chế trong câu trả lời khi chưa hiểu sâu.
  • 〜たまま (N3): Cứ để nguyên trạng thái…
    • Cấu trúc: V-た + まま
    • Sắc thái sử dụng: 理由がふわっとしたままなので = Vì lý do cứ để nguyên ở trạng thái mơ hồ.
  • 〜によって (N3): Tùy thuộc vào…
    • Cấu trúc: Danh từ + によって
    • Sắc thái sử dụng: 相手の状況や回線の速さによって = Tùy thuộc vào tình trạng phía đối phương và tốc độ mạng.
  • 〜といった (N3): Những thứ như là A, B…
    • Cấu trúc: A や B といった + Danh từ
    • Sắc thái sử dụng: Dùng để liệt kê các ví dụ đại diện.
  • 〜わけではない (N3): Không hẳn là…, không có nghĩa là…
    • Cấu trúc: Mệnh đề thông thường + わけではない
    • Sắc thái sử dụng: 処理が早くなるわけではなく = Không có nghĩa là tốc độ xử lý sẽ nhanh hơn.
  • 〜とおり (N3): Đúng như…
    • Cấu trúc: Danh từ の / V-た + とおり
    • Sắc thái sử dụng: 引用にもあったとおり = Đúng như đã nêu trong phần trích dẫn.
  • 〜だとしたら (N2): Giả sử nếu là…
    • Cấu trúc: Thể thông thường + としたら
    • Sắc thái sử dụng: これがWebアプリケーションだとしたら = Giả sử nếu đây là một ứng dụng Web thực tế.
Quay lại danh sách bài viết
架
Ban biên tập BrSE.vnKỹ sư Cầu nối & Chuyên gia Dự án Nhật - Việt

Đội ngũ biên tập BrSE.vn gồm các Bridge Software Engineer (BrSE) và Solution Architect với hơn 10 năm kinh nghiệm thực chiến trong các dự án hợp tác công nghệ Việt - Nhật. Chia sẻ kiến thức về kỹ thuật làm rõ yêu cầu (要件定義), tài liệu đặc tả hệ thống, lược dịch chuyên sâu từ Qiita và kỹ năng cầu nối song hành công nghệ - nghiệp vụ.