第4章 异步编程与事件循环高频题

异步编程是 Node.js 高并发的基石,面试中约 70% 的题目围绕回调、Promise、async/await 及事件循环的执行顺序。本章从底层机制到实战陷阱,搭建完整回答框架。

高频考点权重: 事件循环(40%)→ Promise 链/错误处理(30%) → async/await 并发控制(20%) → 回调地狱解决(10%)
事件循环回答框架 ① 阶段顺序 ② 宏/微任务 ③ 举例分析 ④ 边界陷阱 ⑤ 总结 timer/poll/check micro先于macro 常见面试代码 nextTick/await 输出顺序
面试策略: 先用框架“阶段→宏微任务→举例→边界→总结”组织语言,避免想到哪说到哪。遇到复杂输出题,边画边分析更稳。

🔹 问答卡片 1

面试问题请解释 Node.js 的事件循环机制,并给出以下代码的输出:
setTimeout(() => console.log(1), 0);
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
console.log(4);
面试官考察点1️⃣ 事件循环各阶段(timers/poll/check)的理解 2️⃣ 宏任务与微任务的执行顺序 3️⃣ nextTick 的特殊优先级
回答框架
  1. 定调:Node.js 事件循环基于 libuv,分 6 个阶段,每个阶段有一个宏任务队列,每个阶段结束后清空微任务队列。
  2. 区分优先级:process.nextTick 优先级高于 Promise 微任务,属于本轮循环结束前执行。
  3. 逐行为:同步代码(4)→ 微任务(3→2)→ 宏任务(1)。
  4. 特殊情况:setTimeout(fn,0) 其实会延迟 ~1ms,但可视为进入下一轮 timer 阶段。
简短示例回答输出顺序:4 → 3 → 2 → 1。首先同步代码 console.log(4);接着 process.nextTick 在微任务中优先级最高,打印 3;然后 Promise.then 打印 2;最后 timer 宏任务执行,打印 1。
可能追问🔸 如果 setTimeout 和 setImmediate 同时出现,输出顺序取决于什么?
🔸 process.nextTick 和 Promise.then 哪个先执行?为什么?
避坑提醒⚠️ 不要把 process.nextTick 当成微任务,它是“本轮检查队列”,在每次事件循环切换阶段前运行,可能造成 IO 饥饿。
⚠️ setTimeout(fn,0) 并不严格为 0ms,受系统定时器精度影响。

🔹 问答卡片 2

面试问题以下 Promise 链输出什么?如果 reject 未被 catch 会怎样?
Promise.resolve('A')
  .then(res => { throw new Error('B'); })
  .then(res => console.log(res))
  .catch(err => console.log(err.message))
  .then(() => console.log('C'));
面试官考察点1️⃣ Promise 链的传递机制 2️⃣ throw 与 reject 的等价性 3️⃣ catch 后是否能继续链式调用 4️⃣ 全局 unhandledRejection 事件
回答框架
  1. 画链图:每个 .then 返回新 Promise,reject 会跳过同链后续 then 直到 catch。
  2. 执行路线:resolve('A') 进入第一个 then → 抛出 Error → 进入 catch → 打印 'B' → 返回 undefined → 下一个 then 打印 'C'。
  3. 全局兜底:若链中没有 catch,Node 会触发 'unhandledRejection' 事件,建议全局监听并记录。
简短示例回答输出:B → C。第一个then抛错,跳过第二个then,被catch捕获打印'B';catch返回undefined,下一个then打印'C'。如果catch缺失,错误会成为未捕获的Promise rejection,建议使用.on('unhandledRejection')处理。
可能追问🔸 .then(onResolved, onRejected) 与 .catch(fn) 有什么区别?
🔸 async/await 中如何捕获 Promise 的 reject?
避坑提醒⚠️ return 错误对象与 throw 错误不同:return new Error() 会作为成功值传递。
⚠️ 不要在 Promise 外层使用 try/catch 包裹,需要使用链式 catch 或 await 的方式。

🔹 问答卡片 3

面试问题如何用 async/await 并发执行多个 HTTP 请求?如果有一个请求失败,如何让其他请求不受影响?
面试官考察点1️⃣ Promise.all 与 Promise.allSettled 的区别 2️⃣ 并发控制与错误隔离 3️⃣ 微任务在 await 内部的执行时机
回答框架
  1. 并发发起:使用 Promise.all(urls.map(fetch)) 或 Promise.allSettled。
  2. 错误隔离:若需单个失败不拖垮全部,用 Promise.allSettled 或每条请求加 catch 返回默认值。
  3. 控制并发数:用 for 循环 + 队列模式或 p-limit 库限制最大并发。
简短示例回答使用 Promise.allSettled 并发请求,每条请求通过 .catch() 转为默认结果。如果想控制并发数(如限制5个),可用一个for循环每次Promise.all(5个请求),或者使用p-limit库。
可能追问🔸 await 在事件循环中如何影响微任务执行顺序?
🔸 for 循环中对每个 await 是否会变成串行?如何保持并发?
避坑提醒⚠️ 不要在循环体内直接 await,如 for (const url of urls) { await fetch(url) } 会变成串行,性能低下。
⚠️ Promise.all 中任何一个 reject 会导致整体 reject,务必使用 allSettled 或独立 catch。

📦 项目案例:高并发 API 网关中的异步流程控制

项目名称企业级流量网关服务(中间层)
业务背景为内部微服务提供统一的鉴权、限流、日志、请求聚合功能,前端通过网关转发到后端。网关需要处理大量异步 RPC 调用(如更新缓存、埋点、异步通知)。
规模指标日均请求 2亿+,峰值 QPS 5000,平均响应时间 < 50ms,涉及 30+ 下游服务。
技术栈Node.js 14+,Express + cluster 模式,Redis 限流,p-limit 控制并发,Winston 日志,Sentry 错误监控。
核心难点
  • 异步 IO 密集型下避免事件循环阻塞:大量同步日志写入导致 timer 延迟,改用流式写入+缓冲区。
  • 并发控制:对下游服务的请求限流,防止雪崩,使用令牌桶 + p-limit。
  • 错误传播链:某个下游超时不影响主请求,使用 Promise.race + 超时包装,失败后只记录 error(不 reject)。
  • 全局 unhandledRejection 监控,避免未捕获的 Promise 错误导致进程退出。
面试高频追问
  • 你们如何保证 event loop 不被耗时同步操作阻塞?(答:拆分大任务、使用 worker_threads 或 setImmediate 让出事件循环。)
  • 当业务需要批量异步更新缓存时,如何控制并发数?
  • 如果某个异步任务返回了 rejected promise,但又不想影响主流程,怎么处理?
可迁移回答话术“在网关中,我们使用 p-limit 控制下游并发数,对每个异步请求独立 catch 并降级处理。这样即使某个服务返回 503,主请求仍能返回降级数据。我们还在全局监听 unhandledRejection 和 uncaughtException,确保进程稳定。关键点:别让一个异步错误拖垮整个 Node 进程。”
面试实战提示: 项目案例中说“我们用了 Promise.allSettled” 比 说“我们用了 try/catch” 更体现技术深度。记得强调降级策略和事件循环的健康度监控。
追问路径图 事件循环 → Promise → async/await → 错误处理 宏/微任务顺序 Promise链错误传播 并发控制与性能 setImmediate vs setTimeout nextTick 饥饿 catch与then(onRejected)区别 unhandledRejection await对微任务影响 p-limit原理 面试中每回答一层,面试官可能向下一级深入
本章练习建议: 手写 5 道事件循环输出题,结合 SVG 框架逐步分析;然后把 Promise 链错误处理写成代码验证;最后用 Node.js 实现一个带并发控制的异步任务调度器。