Node.js 对前端来说是工程化、服务端渲染、BFF 和全栈能力的关键补充。面试重点是运行机制和服务稳定性。

运行机制

EventLoop

知识点讲解

Node 的事件循环由 libuv 驱动,包含 timers、pending callbacks、idle/prepare、poll、check、close callbacks 等阶段。setTimeout 在 timers 阶段,setImmediate 在 check 阶段,Promise 微任务会在当前阶段结束后尽快清空。

面试常问

问:setTimeoutsetImmediate 谁先执行?

答:不一定。顶层代码里受定时器精度和事件循环进入时机影响;如果在 I/O 回调里,setImmediate 通常先于 setTimeout

Buffer

知识点讲解

Buffer 用来处理二进制数据。Node 在网络、文件、加密场景中经常需要直接操作字节,字符串只是按编码解释后的结果。

面试常问

问:Buffer 和字符串有什么区别?

答:Buffer 是字节序列,字符串是字符序列。一个字符可能对应多个字节,比如 UTF-8 中文通常占 3 个字节。

Stream

知识点讲解

Stream 用分块方式处理数据,避免一次性把大文件读入内存。常见类型有 Readable、Writable、Duplex、Transform。背压机制能避免写入方过快导致内存膨胀。

面试常问

问:为什么大文件上传下载要用 Stream?

答:可以边读边写,降低内存占用,并通过背压协调读写速度,提升稳定性。

网络服务

HTTP Server

知识点讲解

Node 内置 http 模块可以创建服务。框架如 Express、Koa、NestJS 封装了路由、中间件、异常处理和生态能力。

面试常问

问:Koa 洋葱模型是什么?

答:中间件按顺序进入,遇到 await next() 后进入下一个中间件,后续中间件完成后再回到上一层继续执行,形成类似洋葱的前后置处理。

WebSocket、GraphQL 与 gRPC

知识点讲解

WebSocket 适合实时双向通信。GraphQL 让客户端声明需要的数据结构,适合复杂聚合查询。gRPC 基于 HTTP/2 和 Protobuf,适合服务间高性能通信。

面试常问

问:WebSocket 和 HTTP 轮询区别?

答:轮询是客户端定时请求,实时性和资源利用都较差;WebSocket 建立长连接后可双向推送,适合聊天、协作、实时通知。

并发与稳定性

Cluster

知识点讲解

Node 单进程单主线程,CPU 密集任务会阻塞事件循环。Cluster 可以启动多个进程利用多核 CPU,通常由主进程管理 worker。

面试常问

问:Node 适合 CPU 密集任务吗?

答:不适合放在主线程直接跑。可以用 Worker Threads、子进程、任务队列,或者交给专门服务处理。

Worker Threads

知识点讲解

Worker Threads 提供线程级并行能力,适合 CPU 密集计算。它和主线程之间通过消息通信,也可以使用 SharedArrayBuffer 共享内存。

面试常问

问:Worker Threads 和 Cluster 区别?

答:Cluster 是多进程,适合扩展服务吞吐;Worker Threads 是多线程,适合分担 CPU 密集计算。

错误处理与日志

知识点讲解

服务端要处理同步错误、异步 rejection、接口异常、超时、重试和日志追踪。生产环境不能只靠 console.log,需要结构化日志、请求 ID、监控告警。

面试常问

问:Node 服务如何防止崩溃?

答:业务层做好 try/catch 和统一错误处理中间件,处理未捕获异常并上报,进程异常后由 PM2、容器或守护进程拉起,同时保留日志和告警。

底层追问与代码示例

Node 事件循环阶段细节

知识点讲解

Node 的 process.nextTick 不属于普通微任务队列,它优先级高于 Promise 微任务。滥用 nextTick 递归会饿死 I/O。

1
2
3
4
5
6
7
setTimeout(() => console.log('timer'), 0)
setImmediate(() => console.log('immediate'))

Promise.resolve().then(() => console.log('promise'))
process.nextTick(() => console.log('nextTick'))

console.log('sync')

常见输出:

1
2
3
4
sync
nextTick
promise
timer / immediate 顺序不固定

面试常问

问:为什么不要大量递归调用 process.nextTick?

答:它会在事件循环进入下一阶段前持续清空,可能导致 timers、I/O、check 阶段迟迟无法执行。

Stream 背压代码示例

知识点讲解

write() 返回 false 表示写入缓冲区已满,读取方应该暂停,等 drain 再继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs')

const reader = fs.createReadStream('./large.log')
const writer = fs.createWriteStream('./copy.log')

reader.on('data', chunk => {
const canContinue = writer.write(chunk)
if (!canContinue) {
reader.pause()
}
})

writer.on('drain', () => {
reader.resume()
})

reader.on('end', () => {
writer.end()
})

更推荐使用 pipeline,它会处理背压和错误传播:

1
2
3
4
5
6
7
8
9
const { pipeline } = require('stream/promises')
const fs = require('fs')

async function copy() {
await pipeline(
fs.createReadStream('./large.log'),
fs.createWriteStream('./copy.log')
)
}

CPU 密集任务放到 Worker

知识点讲解

CPU 密集任务会阻塞主线程,让所有请求变慢。Worker Threads 可以把计算移出主线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
const { Worker } = require('worker_threads')

function runWorker(payload) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: payload })
worker.on('message', resolve)
worker.on('error', reject)
worker.on('exit', code => {
if (code !== 0) reject(new Error(`Worker stopped: ${code}`))
})
})
}
1
2
3
4
5
6
7
8
// worker.js
const { parentPort, workerData } = require('worker_threads')

function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}

parentPort.postMessage(fib(workerData.n))

面试常问

问:Node 高并发靠什么?

答:靠事件驱动和非阻塞 I/O 处理大量 I/O 并发,不是靠单线程执行 CPU 任务很快。CPU 密集计算仍要拆出去。