Bài 4. Bất đồng bộ, event loop, sự kiện và stream

Hiểu cơ chế bất đồng bộ là chìa khóa viết Node.js đúng. Bài này giải thích event loop trong Node.js, các mẫu callback → Promise → async/await, xử lý song song, EventEmitter và stream để xử lý tệp lớn mà không tốn bộ nhớ.

50 phútTrung cấp
Bạn sẽ học được
  • Giải thích event loop và thứ tự thực thi
  • Dùng async/await, Promise.all, Promise.allSettled, xử lý lỗi
  • Dùng EventEmitter
  • Đọc ghi tệp lớn bằng stream và pipeline

Một số khái niệm

Node.js chạy mã JavaScript trên một luồng. Khi gặp thao tác I/O, Node giao cho hệ điều hành/libuv và tiếp tục chạy mã khác; khi I/O xong, callback được xếp vào hàng đợi và event loop lấy ra chạy. Vì vậy: không viết vòng lặp tính toán nặng trong máy chủ (sẽ “đóng băng” mọi yêu cầu), hãy chuyển sang worker_threads hoặc dịch vụ riêng.

async/await và song song

import { setTimeout as cho } from 'node:timers/promises';

async function taiDiem(ma) { await cho(500); return { ma, diem: Math.round(Math.random() * 10) }; }

// Tuần tự: ~1500ms
for (const ma of ['SV1', 'SV2', 'SV3']) console.log(await taiDiem(ma));

// Song song: ~500ms
const kq = await Promise.all(['SV1', 'SV2', 'SV3'].map(taiDiem));

// Không dừng khi một tác vụ lỗi
const tatCa = await Promise.allSettled([taiDiem('SV1'), Promise.reject(new Error('Mạng lỗi'))]);
tatCa.forEach(r => console.log(r.status, r.value ?? r.reason.message));

// Xử lý lỗi
try { await fetch('https://khong-ton-tai.invalid'); }
catch (e) { console.error('Lỗi:', e.message); }

EventEmitter

import { EventEmitter } from 'node:events';
class LopHoc extends EventEmitter {
  themSinhVien(ten) { this.emit('them', ten); }
}
const lop = new LopHoc();
lop.on('them', ten => console.log('Chào mừng', ten));
lop.once('them', () => console.log('Sinh viên đầu tiên!'));
lop.themSinhVien('An'); lop.themSinhVien('Bình');

Rất nhiều đối tượng Node.js là EventEmitter: máy chủ HTTP (sự kiện request), stream (data, end), process (exit).

Stream: xử lý dữ liệu lớn

import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { pipeline } from 'node:stream/promises';
import readline from 'node:readline';

// Nén tệp log 2GB mà chỉ dùng vài MB bộ nhớ
await pipeline(createReadStream('nhat-ky.log'), createGzip(), createWriteStream('nhat-ky.log.gz'));

// Đọc CSV từng dòng
const rl = readline.createInterface({ input: createReadStream('diem.csv') });
let dem = 0;
for await (const dong of rl) { if (dem++ === 0) continue; const [ma, ten, diem] = dong.split(','); /* xử lý */ }
console.log('Đã đọc', dem - 1, 'dòng');

Stream chia dữ liệu thành từng khối nhỏ; pipeline nối các stream và tự xử lý lỗi, đóng tệp.

Mẹo hay

Đo thời gian với console.time("tai") / console.timeEnd("tai") để thấy rõ khác biệt tuần tự và song song.

Lưu ý

Quên await làm lỗi bị “nuốt” (unhandled rejection) và thứ tự chạy sai; bật ESLint rule no-floating-promises khi dùng TypeScript.

Thực hành
  1. Gọi 5 API song song bằng Promise.all và đo thời gian.
  2. Tạo EventEmitter thông báo khi có điểm mới.
  3. Nén một tệp lớn bằng pipeline.
  4. Đọc tệp CSV 10.000 dòng, tính điểm trung bình bằng readline.
Tóm tắt bài học
  • Một luồng + event loop + I/O không chặn.
  • async/await, Promise.all/allSettled, try/catch.
  • EventEmitter: on, once, emit.
  • Stream + pipeline cho dữ liệu lớn.