Interview Prep
PracticePortfolio

Categories

  • HTML & CSS41
  • JavaScript40
  • TypeScript42
  • ReactJS40
  • Next.js44
  • Node.js41
  • NestJS44
  • SQL & Database43
  • MongoDB40
  • Git40
  • Docker44
  • GraphQL47
Interview PrepNode.js

Node.js

41 questions

20 / 41 questions

1

Node.js là runtime chạy JavaScript ngoài trình duyệt, dựa trên engine V8 của Chrome kết hợp thư viện libuv để lo I/O bất đồng bộ. Khác trình duyệt, Node không có DOM/window mà cung cấp API hệ thống: fs, net, http, process, Buffer... Nó hướng tới xây dựng server, tool CLI và dịch vụ backend.

2

Node chạy JS đơn luồng nhưng đẩy các tác vụ I/O (đọc file, network) xuống libuv để xử lý bất đồng bộ, rồi nhận kết quả qua callback/Promise. Nhờ vậy một luồng phục vụ được rất nhiều request đồng thời mà không chờ (block) từng cái. Điều cần tránh là code CPU-bound chặn event loop.

3

Event loop là vòng lặp do libuv điều khiển, liên tục lấy các callback đã sẵn sàng ra khỏi hàng đợi và chạy trên luồng chính. Nó cho phép Node xử lý bất đồng bộ dù JS đơn luồng: khi một I/O xong, callback của nó được xếp vào hàng đợi để event loop pick lên. Nếu code đồng bộ nặng, event loop bị block và không xử lý được việc khác.

4

Mỗi vòng lặp đi qua các phase theo thứ tự: timers (setTimeout/setInterval), pending callbacks (một số callback I/O bị hoãn), poll (lấy sự kiện I/O mới, chạy callback đọc/ghi), check (setImmediate), và close callbacks (ví dụ socket.on('close')). Giữa mỗi phase, Node còn xử lý hàng đợi microtask (process.nextTick và Promise) trước khi sang phase kế tiếp.

5

libuv là thư viện C cung cấp event loop và lớp trừu tượng I/O đa nền tảng cho Node. Với các tác vụ không có API bất đồng bộ ở tầng OS (như fs, DNS lookup, một số thuật toán crypto, zlib), libuv dùng một thread pool (mặc định 4 luồng) để chạy song song rồi trả kết quả về event loop. Có thể tăng số luồng qua biến UV_THREADPOOL_SIZE.

6

process.nextTick chạy ngay sau thao tác hiện tại, trước khi event loop sang phase khác — nó là microtask ưu tiên cao nhất. setImmediate chạy trong phase check (sau poll). setTimeout(fn, 0) chạy trong phase timers với độ trễ tối thiểu ~1ms. Lạm dụng process.nextTick đệ quy có thể starve (bỏ đói) I/O vì event loop không kịp sang phase khác.

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
// nextTick -> (timeout|immediate tùy ngữ cảnh)
7

Microtask gồm callback của Promise (.then/await) và process.nextTick; chúng được vét sạch giữa các phase của event loop. Macrotask là các callback nằm trong phase như timers (setTimeout), check (setImmediate), poll (I/O). Quy tắc: sau mỗi macrotask, Node chạy hết hàng đợi microtask rồi mới tiếp tục — trong đó process.nextTick được ưu tiên hơn cả Promise.

8

Stream là abstraction để xử lý dữ liệu theo từng phần (chunk) thay vì nạp toàn bộ vào bộ nhớ, rất hợp cho file lớn hay network. Có 4 loại: Readable (nguồn đọc, ví dụ fs.createReadStream), Writable (đích ghi), Duplex (đọc và ghi, như TCP socket), và Transform (Duplex có biến đổi dữ liệu, như zlib.createGzip). Stream giúp tiết kiệm RAM và bắt đầu xử lý sớm.

9

Backpressure xảy ra khi nguồn đọc dữ liệu nhanh hơn tốc độ đích ghi có thể tiêu thụ, khiến buffer phình to và tốn RAM. .pipe() (và stream.pipeline) tự động điều tiết: khi Writable trả false từ write(), Readable tạm dừng cho đến khi có sự kiện drain. Nên dùng pipeline() vì nó xử lý lỗi và dọn dẹp resource tốt hơn pipe().

const { pipeline } = require('stream/promises');
await pipeline(readStream, gzip, writeStream);
10

Buffer là vùng bộ nhớ nhị phân cố định (raw binary), nằm ngoài heap của V8, dùng để xử lý dữ liệu byte như file, TCP, mã hoá. Nó ra đời trước khi JS có TypedArray/Uint8Array và nay là lớp con của Uint8Array. Dùng Buffer.from() để tạo và luôn tránh new Buffer() (deprecated, không an toàn). Khi xử lý text nên khai báo rõ encoding, ví dụ buf.toString('utf8').

11

EventEmitter là lớp cốt lõi cho mô hình publish/subscribe trong Node: đăng ký listener với .on(event, cb) và phát sự kiện bằng .emit(event, ...args). Rất nhiều API dựa trên nó (stream, http.Server, process). Cần lưu ý cảnh báo memory leak khi vượt maxListeners (mặc định 10) và luôn xử lý sự kiện 'error', nếu không nó sẽ throw và crash tiến trình.

const { EventEmitter } = require('events');
const bus = new EventEmitter();
bus.on('done', (id) => console.log('xong', id));
bus.emit('done', 42);
12

Callback là hàm truyền vào để gọi lại khi tác vụ bất đồng bộ hoàn tất. Node dùng quy ước error-first: tham số đầu là lỗi (err), các tham số sau là kết quả. Nếu err khác null thì phải xử lý lỗi trước và return sớm. Lồng nhiều callback dễ gây callback hell, nên ngày nay ưu tiên Promise/async-await.

fs.readFile('a.txt', (err, data) => {
  if (err) return console.error(err);
  console.log(data.toString());
});
13

util.promisify chuyển một hàm theo kiểu error-first callback thành hàm trả về Promise, giúp dùng được với async/await. Nó hữu ích khi thư viện cũ chưa hỗ trợ Promise. Nhiều module core nay đã có sẵn phiên bản Promise (ví dụ fs/promises), nên ưu tiên dùng bản đó.

const { promisify } = require('util');
const sleep = promisify(setTimeout);
await sleep(1000);
14

Vì Node chạy đơn luồng nên một tiến trình chỉ tận dụng được một CPU core. Cluster fork nhiều tiến trình con (worker) cùng chia sẻ một cổng lắng nghe, master phân phối kết nối để tận dụng đa lõi. Các worker không chia sẻ bộ nhớ, nên state chung phải để ở Redis/DB. Trên môi trường container, người ta thường thay cluster bằng nhiều instance sau một load balancer.

15

worker_threads cho phép chạy JS trên nhiều luồng thật trong cùng một tiến trình, phù hợp với công việc CPU-bound (tính toán nặng, xử lý ảnh) để không chặn event loop chính. Các luồng giao tiếp qua postMessage/MessagePort và có thể chia sẻ bộ nhớ qua SharedArrayBuffer. Khác cluster: worker_threads là luồng trong một process, còn cluster là nhiều process riêng biệt.

16

spawn chạy một lệnh và stream stdout/stderr — tốt cho output lớn/lâu dài. exec chạy lệnh qua shell rồi buffer toàn bộ output (dễ tràn nếu output lớn, và có nguy cơ command injection nếu ghép chuỗi). fork là biến thể chuyên chạy một file Node con, mở sẵn kênh IPC để gửi message giữa cha–con. Dùng spawn với mảng đối số để tránh injection.

17

CommonJS dùng require/module.exports, nạp đồng bộ và tính giá trị lúc gọi. ESM dùng import/export, phân tích tĩnh, hỗ trợ top-level await và tree-shaking; kích hoạt bằng "type": "module" trong package.json hoặc đuôi .mjs. ESM là bất đồng bộ và không có require mặc định. Node mới cho phép require() một ESM đồng bộ trong một số trường hợp, nhưng nên thống nhất một hệ.

18

Từ ESM bạn có thể import một module CommonJS: module.exports được ánh xạ thành default export, và named export chỉ hoạt động nếu Node phân tích tĩnh được. Ngược lại, gọi ESM từ CJS truyền thống phải dùng dynamic import() (trả Promise) vì ESM nạp bất đồng bộ. Tránh trộn lẫn hai hệ trong cùng file; chọn một chuẩn cho toàn dự án để giảm rắc rối.

19

Khi bạn require() một module CommonJS lần đầu, Node thực thi nó rồi cache kết quả trong require.cache; các lần require sau trả về đúng đối tượng đã cache (singleton). Nhờ đó module chỉ chạy một lần và có thể giữ state chung. Xoá cache (delete require.cache[...]) để reload là mẹo cho dev nhưng dễ gây rò rỉ/không nhất quán, không nên dùng ở production.

20

ESM không có sẵn __dirname/__filename như CommonJS. Node mới cung cấp import.meta.dirname và import.meta.filename. Với Node cũ hơn, dựng lại từ import.meta.url.

import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);