文章目录
- 前言
前言
结合 Node.js 的核心机制进行说明:
-
解释事件循环的各个阶段。
答案
Node.js 事件循环分为 6 个阶段,按顺序执行: -
Timers:执行
setTimeout
和setInterval
的回调。 -
Pending I/O Callbacks:处理系统操作(如 TCP 错误)的回调。
-
Idle/Prepare:Node.js 内部使用的阶段。
-
Poll:
• 检索新的 I/O 事件并执行回调(如文件读取、HTTP 请求)。• 如果 Poll 队列为空:
◦ 若有
setImmediate
回调,进入 Check 阶段。◦ 否则等待新的 I/O 事件。
-
Check:执行
setImmediate
的回调。 -
Close Callbacks:处理关闭事件的回调(如
socket.on('close')
)。
解析
• 每个阶段都是一个 FIFO 队列,必须清空当前阶段的回调才会进入下一阶段。
• 重点:setTimeout
和 setInterval
的回调不一定精确按时执行,因为 Poll 阶段可能阻塞事件循环。
setImmediate
和setTimeout(fn, 0)
的区别是什么?
答案
• 执行顺序:
• 在主模块中,两者的执行顺序不确定(受进程性能影响)。
• 在 I/O 回调(如 fs.readFile
)中,setImmediate
总是先于 setTimeout
。
• 底层阶段:
• setImmediate
在 Check 阶段 执行。
• setTimeout
在 Timers 阶段 执行。
示例代码
fs.readFile('file.txt', () => {
setTimeout(() => console.log('Timeout'), 0);
setImmediate(() => console.log('Immediate'));
});
// 输出顺序:Immediate → Timeout
解析
• 在 I/O 回调中,事件循环处于 Poll 阶段,执行完回调后优先进入 Check 阶段(setImmediate
),再进入 Timers 阶段。
- 什么是事件驱动编程?Node.js 如何实现非阻塞 I/O?
答案
• 事件驱动:通过监听事件(如点击、文件读取完成)触发回调,而非主动轮询。
• 非阻塞 I/O 的实现:
• 操作系统级异步:网络请求等由内核异步处理(通过 epoll
、kqueue
)。
• 线程池:文件 I/O 等阻塞操作由 libuv 的线程池处理,完成后通知主线程。
解析
• Node.js 的单线程仅指 JS 主线程,底层通过多线程 + 事件循环实现高并发。
- 如何监控和调试内存泄漏?
答案
常见泄漏场景:
• 未清理的全局变量、闭包引用、定时器、事件监听器(如 EventEmitter
)。
调试工具:
• Chrome DevTools 的 Heap Snapshot 对比内存快照。
• 使用 --inspect
参数 + node-heapdump
模块生成堆内存快照。
• 监控 process.memoryUsage()
。
解析
• 内存泄漏的本质是对象被意外保留,无法被 GC 回收。
process.nextTick
和setImmediate
的执行顺序?
答案
•process.nextTick
:
• 在事件循环的每个阶段结束后立即执行(微任务)。
• 优先级高于 Promise.then()
。
• setImmediate
:
• 在 Check 阶段执行(宏任务)。
执行顺序:
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('Immediate'));
// 输出顺序:nextTick → Promise → Immediate
解析
• process.nextTick
会将回调插入当前阶段末尾,而 setImmediate
是下一轮循环。
- Node.js 单线程模型如何处理并发请求?
答案
• 非阻塞 I/O:主线程发起异步 I/O 操作后继续处理其他任务,I/O 完成后通过事件循环触发回调。
• 线程池:文件操作等阻塞任务由 libuv 的线程池处理(默认 4 个线程)。
解析
• 单线程避免了多线程的锁竞争和上下文切换开销,适合 I/O 密集型场景,但不适合 CPU 密集型任务。
- Cluster 模块是如何工作的?
答案
• 原理:Master 进程创建多个子进程(Worker),共享同一端口,通过轮询(Round-Robin)分配请求。
• 代码示例:
const cluster = require('cluster');
if (cluster.isMaster) {
for (let i = 0; i < 4; i++) cluster.fork(); // 启动 4 个 Worker
} else {
require('./app.js'); // 每个 Worker 运行一个服务实例
}
解析
• 子进程通过 IPC(进程间通信)与 Master 进程通信。
• 优势:利用多核 CPU,提高吞吐量。
- Buffer 和 Stream 的应用场景是什么?
答案
• Buffer:处理二进制数据(如图片、文件),避免字符串转换的性能损耗。
• Stream:
• 大文件处理:分片读取文件,避免内存溢出(如 fs.createReadStream
)。
• 实时数据传输:HTTP 响应、TCP 套接字。
示例
// 使用 Stream 复制文件
fs.createReadStream('input.txt')
.pipe(fs.createWriteStream('output.txt'));
解析
• Stream 通过事件分块处理数据,显著降低内存占用。
总结
掌握这些问题的核心原理(事件循环、异步 I/O、内存管理)能让你在面试中脱颖而出。建议结合以下实践:
- 使用
node --trace-event-categories=node.async_hooks
跟踪异步事件。 - 阅读 libuv 文档 和 Node.js 官方博客。
- 通过
WARTHOG
(Node.js 性能分析工具)定位性能瓶颈。