Node.js 核心面经
Node.js 深水区知识
1. 事件循环深度
6 个阶段
timers -> setTimeout / setInterval
pending I/O -> 上一转延迟的 I/O
idle, prepare -> 内部使用
poll -> 检索新 I/O,执行 I/O 回调
check -> setImmediate
close callbacks -> socket.destroy() 等
每个阶段之前,清空 microtask 队列:
process.nextTick 队列 -> Promise 微任务队列
经典面试题:输出顺序
setTimeout(() => console.log('setTimeout'), 0)
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
console.log('sync')
// 输出顺序:
// sync
// nextTick
// Promise
// setTimeout (在 I/O 外面,timeout 和 immediate 顺序不定)
// setImmediate
poll 阶段详解
- 如果 timer 队列非空,进入 timers 阶段
- 如果进入 poll 时没有 timer,直接执行 poll 回调
- 如果 poll 为空且有 setImmediate,进入 check 阶段
2. 模块系统
CommonJS vs ESM
// CommonJS:运行时加载,同步
const fs = require('fs') // 运行时才执行
exports.greet = name => `Hello ${name}`
// ESM:静态分析,异步
import fs from 'fs' // 编译时分析
export const greet = name => `Hello ${name}`
// 主要区别
// 1. CommonJS 是值导出,ESM 是实时绑定
// 2. CommonJS require 可在中途调用,import 必须在顶层
// 3. ESM 有利于 tree-shaking
const value = require('./module').value // CommonJS: value 是拷贝
import { value } from './module' // ESM: value 是引用
模块寻址顺序
1. 核心模块(Buffer、path...)
2. 路径模块(./、../、/开头)
3. node_modules 查找(逐级向上)
3. 性能优化
集群模式
// cluster 模块利用多核 CPU
import cluster from 'cluster'
import { cpus } from 'os'
if (cluster.isPrimary) {
const numCPUs = cpus().length
for (let i = 0; i < numCPUs; i++) {
cluster.fork() // 每个 CPU 核开一个 Worker
}
cluster.on('exit', (worker) => {
cluster.fork() // Worker 崩溃自动重启
})
} else {
// Worker 进程运行 HTTP 服务
app.listen(3000)
}
Worker Threads 处理 CPU 密集任务
import { Worker, isMainThread, parentPort } from 'worker_threads'
if (isMainThread) {
const worker = new Worker(__filename)
worker.on('message', result => console.log('Result:', result))
worker.postMessage({ data: heavyData })
} else {
parentPort.on('message', ({ data }) => {
const result = heavyComputation(data) // CPU 密集运算
parentPort.postMessage(result)
})
}
内存泄漏排查
# 开启内存分析
node --inspect app.js
# 姳片分析
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>
# 常见泄漏场景:
# 1. 全局变量累积
# 2. 闭包持有外部引用未释放
# 3. 事件监听器未移除
4. 流处理与管餅
// 流序列化:管餅
readStream
.pipe(gunzip()) // 解压
.pipe(csvParser()) // 解析
.pipe(transform()) // 转换
.pipe(writeStream) // 写入
// 现代写法:pipeline
const { pipeline } = require('stream/promises')
await pipeline(
fs.createReadStream('input.gz'),
zlib.createGunzip(),
fs.createWriteStream('output')
)
// 自动处理错误和清理
// 备压处理
readStream.on('data', chunk => {
if (!writeStream.write(chunk)) {
readStream.pause() // 写入缓冲达到高水位,暂停读取
writeStream.once('drain', () => readStream.resume())
}
})
5. 资深面试题
- 如何处理运算密集任务而不阻塞事件循环?
- Worker Threads 处理 CPU 密集
- child_process.fork() 开子进程
- 将任务切片,用 setImmediate/setTimeout 分批执行
- require 模块是否每次都重新执行?
- 不是,模块有缓存机制(require.cache)
- 第一次 require 执行并缓存,后续直接返回缓存
- 删除缓存:delete require.cache[require.resolve('./module')]
- 如何优化 Node.js 应用的内存占用?
- 调整 --max-old-space-size 设置堆内存上限
- 大文件用 Stream 处理,避免全部读入内存
- 缓存设置合理的 TTL,防止无限增长
- process.nextTick 和 setImmediate 的应用场景?
- nextTick:当前操作完成后立即执行,高于其他微任务
- setImmediate:在当前 poll 阶段完成后执行
- nextTick 如果递归调用可能阻塞事件循环,要注意