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ớ.
- 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.
Đ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.
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.
- Gọi 5 API song song bằng Promise.all và đo thời gian.
- Tạo EventEmitter thông báo khi có điểm mới.
- Nén một tệp lớn bằng pipeline.
- Đọc tệp CSV 10.000 dòng, tính điểm trung bình bằng readline.
- 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.