异步编程与事件循环:Node 的魂
这一章是 Node.js 最核心、也最容易把人绕晕的地方。先记住一句话:Node 是单线程的,但它能同时招呼一万个请求——秘诀就是"不等"。读文件、查数据库、发网络请求这些"要等"的活,它丢给后台去干,自己接着处理下一个请求。本章就讲这套机制怎么一步步演化成你天天写的 async/await。
第一站:回调函数(Callback),以及它怎么把你坑哭
最早的 Node 用"回调函数"处理异步:你给一个操作传一个函数,等操作做完了,它回头调你这个函数。听着挺好,问题是嵌套深了就变成了这样——史称回调地狱(Callback Hell)。
回调地狱长这样(缩进往右飞,像个箭头)
三层嵌套看着还行,实际项目七八层起步。读代码像在解谜语,错误处理到处 if(err),改一个需求要在九层塔上开刀。于是 Promise 登场了——它把"竖着叠"改成"横着连"。
第二站:Promise,把回调拉成一条链
Promise 是个"承诺容器":它代表一件现在没干完、将来会干完(或失败)的事。用 .then() 接成功、.catch() 接失败,一行一行横着连,再也不缩进了。
同一个逻辑,用 Promise 链式调用
顺手解释三个方法:.then() 是成功后干嘛,.catch() 是出错了干嘛,.finally() 是不管成功失败都干嘛(比如关个加载动画)。
第三站:async/await,写异步像写同步
ES2017 推出 async/await,这是目前你 99% 时间在用的写法。它的魔法是:让"等一个异步结果"看起来和"等一个普通变量"一模一样,再也不用 .then 链条。
同一个逻辑,async/await 版——一眼看懂
划记住两条铁律
铁律一:await 只能在 async 函数(或模块顶层)里用,外面用了直接报错。
铁律二:每一个 await 都应该被 try/catch 包住,或者后面有 .catch——否则一个没捕获的 Promise 报错,Node 15 以后会直接让整个进程崩溃退出。这是新手最常见的线上事故。
事件循环到底在转什么
你不用背下全部细节,但要知道 Node 转一圈(一次事件循环)分六个阶段,像警察绕小区巡逻,每到一个岗亭处理一批活。
| 阶段 | 这一圈在干嘛 |
|---|---|
| timers | 处理 setTimeout、setInterval 到点的回调。 |
| pending callbacks | 处理上一轮延迟到的系统级回调(如 TCP 错误)。 |
| poll(轮询) | 重点岗亭:等新的 I/O 事件(文件读完、网络请求回来),处理它们。没活了就在这等一下。 |
| check | 处理 setImmediate 的回调。 |
| close callbacks | 处理关闭事件,比如 socket.on('close')。 |
宏任务 vs 微任务:执行顺序的"插队规则"
这是面试高频、也是写代码容易踩坑的点。一句话规则:每跑完一个宏任务,就把队列里所有微任务全部清空,再去跑下一个宏任务。
- 宏任务(macrotask):
setTimeout、setInterval、I/O、setImmediate。排队等着轮流跑。 - 微任务(microtask):
Promise.then/.catch/.finally、process.nextTick。优先级极高,当前宏任务一结束立刻全部清空。
看这段,猜一下打印顺序
规律:先跑全部同步代码(1、4);同步跑完,清空微任务(3);最后才轮到宏任务(2)。哪怕 setTimeout 写着 0 毫秒,也得等微任务清空才轮到它。
四个并发帮手:Promise.all / race / allSettled / any
你有三件事可以同时做(比如同时查三个接口),别傻乎乎 await 完一个再等下一个——那样是串行,慢三倍。用这四个方法批量并发:
| 方法 | 什么时候用、什么行为 |
|---|---|
| Promise.all | 三件事一起发,全部成功才成功;任何一个失败就立刻失败。适合"三个数据都拿到才能继续"。 |
| Promise.race | 谁先回来用谁。适合"超时兜底"——设个 3 秒的定时,谁先结束听谁的。 |
| Promise.allSettled | 等所有事都有结果(不管成功失败),拿到一份成绩单。适合"批量发请求,个别失败不影响其他"。 |
| Promise.any | 第一个成功的就用它,全失败才算失败。适合"多个备用接口,哪个通了用哪个"。 |
① 异步写法演进:回调(嵌套地狱)→ Promise(链式)→ async/await(同步观感),现在一律用 async/await。
② 微任务(Promise)比宏任务(setTimeout)先跑,事件循环每圈先清微任务。
③ 能并发就别串行:用 Promise.all 同时发多个请求;每个 await 都记得 try/catch。