Node.js 核心概念精讲:事件循环、异步 I/O、回调与 Promise 的面试回答
本章将拆解事件循环机制、异步 I/O 原理、回调与 Promise 的演进,并给出结构化面试回答框架。重点掌握阶段划分、微任务与宏任务执行顺序、Promise 链与错误处理。
知识结构表:事件循环阶段与优先级
| 阶段 | 执行内容 | 示例 API |
|---|---|---|
| timers | 执行到期定时器回调 | setTimeout / setInterval |
| pending callbacks | 执行延迟到下一轮的 I/O 回调 | 某些系统错误 |
| idle, prepare | 内部使用 | — |
| poll | 轮询新 I/O 事件,执行 I/O 回调 | fs.readFile / net.on('data') |
| check | 执行 setImmediate 回调 | setImmediate |
| close callbacks | 执行关闭事件回调 | socket.on('close') |
关键点:微任务(process.nextTick / Promise)在每个阶段切换时执行,优先级高于宏任务(timers, poll, check)。
面试问题
请解释 Node.js 的事件循环机制,并说明 setTimeout(fn, 0) 与 setImmediate(fn) 的执行顺序差别。
面试官考察点
- 对事件循环阶段的理解深度
- 是否分阶段对比执行顺序
- 能否结合实际代码举例说明
回答框架
- 概述事件循环 6 个阶段,强调 poll 阶段是核心。
- 对比 setTimeout 和 setImmediate:在 poll 阶段后,setImmediate 进入 check 阶段,而 setTimeout 在 timers 阶段执行。
- 特殊场景:在 I/O 回调内,setImmediate 总是先于 setTimeout。
- 补充微任务插入时机。
简短示例回答
“事件循环将工作分为 timers、poll、check 等阶段。setTimeout(fn, 0) 在 timers 阶段执行,setImmediate 在 check 阶段执行。在非 I/O 回调中顺序不确定,但在 I/O 回调内,由于 poll 之后立即进入 check,setImmediate 始终先于 setTimeout。” (约 80 字)
可能追问
- process.nextTick 与 Promise 谁先执行?
- 如何用执行顺序分析死循环场景?
避坑提醒
⚠ 不要笼统说“setImmediate 比 setTimeout 快”,要分场景。同时记住 setTimeout(fn, 0) 在浏览器和 Node.js 中有细微差异。
面试问题
描述 Node.js 异步 I/O 的实现原理,libuv 线程池的作用是什么?
面试官考察点
- 是否理解非阻塞 I/O 与线程池的关系
- 能否区分 CPU 密集型任务与 I/O 密集型任务
- 对 libuv 架构的认知
回答框架
- Node.js 使用 libuv 实现跨平台异步 I/O。
- 对于文件系统、DNS 等操作,libuv 利用线程池(默认 4 个线程)执行阻塞操作。
- 网络 I/O 则依赖 epoll/kqueue 等系统调用,不占用线程池。
- 线程池大小可通过 UV_THREADPOOL_SIZE 调整。
简短示例回答
“异步 I/O 由 libuv 封装系统调用实现。文件操作等通过线程池执行,避免阻塞主线程;网络 I/O 使用操作系统原生异步接口。线程池默认 4 个线程,适合 I/O 密集型任务,不适合计算密集场景。” (约 80 字)
可能追问
- 线程池中的线程会同时处理多个请求吗?
- 如何测试线程池是否够用?
避坑提醒
⚠ 易错点:错误认为所有异步操作都使用线程池。网络 I/O 是真正的非阻塞,文件 I/O 才有线程池。
面试问题
Promise 与 async/await 在处理异步错误时有何不同?如何避免未捕获的 Promise 异常?
面试官考察点
- 对 Promise 链中错误传递的理解
- async/await 的 try/catch 与 Promise 区别
- 全局异常处理机制
回答框架
- Promise 通过 .catch() 捕获,未处理则会触发 unhandledRejection。
- async/await 配合 try/catch 实现同步化错误处理。
- 推荐始终在顶层添加 process.on('unhandledRejection') 兜底。
- 注意 Promise 中 throw 与 reject 的等价性。
简短示例回答
“Promise 用 .catch() 捕获异常,若未捕获会触发 'unhandledRejection'。async/await 通过 try/catch 包裹,语义更清晰。生产中应在进程级别监听 unhandledRejection 并记录日志,避免静默失败。” (约 70 字)
可能追问
- 多个 await 如何并发执行?
- Promise.allSettled 与 Promise.all 的区别?
避坑提醒
⚠ 常见坑:在 async 函数中直接 return 一个 reject 的 Promise,调用方如果不 await 或 catch,异常会丢失。务必在 async 函数体内使用 try/catch 或显式处理。
提示:回答时善用追问路径图预判面试官下一步问题,提前准备“回调→Promise→并发控制”的完整链路。