本节摘要:Node 的异步编程经历了回调函数、Promise、async/await 三代形态。本节按演化顺序逐个拆解:回调为何会堆成地狱、Promise 的状态机如何把「完成通知」变成可组合的值、async/await 又如何把异步流程写回同步的模样。理解每一代解决了什么、留下了什么,比记住 API 更重要。
第 1 章的第一个 HTTP 服务器已经用了回调。对于单个异步操作,回调无可指责:
const fs = require('fs'); fs.readFile('配置.json', 'utf8', (err, data) => { if (err) { console.error('读取失败', err); return; } console.log('内容长度', data.length); });
问题出现在有依赖关系的多个异步步骤时。读配置 → 依据配置查用户 → 依据用户查订单,用回调写出来是这样:
fs.readFile('配置.json', 'utf8', (err, config) => { if (err) return handle(err); db.query('SELECT * FROM users WHERE id = ?', [JSON.parse(config).uid], (err, user) => { if (err) return handle(err); db.query('SELECT * FROM orders WHERE uid = ?', [user.id], (err, orders) => { if (err) return handle(err); if (!orders.length) { // 想再补查一次历史表?继续往右缩进…… } render(user, orders); }); }); });
三层嵌套还只是观感问题,真正要命的是三件事:

Promise 的本质是把「将来才会有的值」包装成对象,让异步结果可以像变量一样被 return、被传递、被组合。它的规则集中在一个状态机里:pending → fulfilled 或 pending → rejected,一旦落定不可逆。
一个足够用的简化实现不到 30 行:
class MiniPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.callbacks = []; const resolve = (v) => { if (this.state !== 'pending') return; // 不可逆 this.state = 'fulfilled'; this.value = v; this.callbacks.forEach(cb => cb(v)); }; const reject = (e) => { if (this.state !== 'pending') return; this.state = 'rejected'; this.value = e; this.callbacks.forEach(cb => cb(e)); }; executor(resolve, reject); } then(onFulfilled) { if (this.state === 'fulfilled') { queueMicrotask(() => onFulfilled(this.value)); } else { this.callbacks.push(v => queueMicrotask(() => onFulfilled(v))); } return this; // 真正的 then 返回新 Promise,这里从简 } }
真实实现要处理 then 返回新 Promise、rejection 传播、thenable 兼容,但骨架就是这样:注册回调 → 状态落定时批量触发。注意 then 里的回调是排进微任务队列的——这个细节下一节会成为顺序推演的主角。
同样的三步查询,Promise 化之后:
const fsp = require('fs').promises; fsp.readFile('配置.json', 'utf8') .then(config => db.query('SELECT * FROM users WHERE id = ?', [JSON.parse(config).uid])) .then(user => db.query('SELECT * FROM orders WHERE uid = ?', [user.id])) .then(orders => render(orders)) .catch(err => handle(err)); // 三层的错误在这里一次收口
嵌套消失了,错误统一了,而且并行也顺手解决了:
// 两个查询并行,总耗时约等于较慢的那个 const [profile, orders] = await Promise.all([ getUserProfile(uid), getUserOrders(uid), ]);
async 函数是生成器 + 自动执行器的语法糖。在引擎眼里,await 后面的代码相当于注册进了一个 then 回调——每次 await 都是一次函数暂停与微任务排队的交接。所以这段代码:
async function loadPage() { const config = await fsp.readFile('配置.json', 'utf8'); const user = await db.query(sql, [uid]); return { config, user }; }
与手写的 Promise 链完全等价。它换来的可读性是真的,但新的坑也是真的:
⚠️ 常见坑:在循环里顺序 await 相互独立的异步操作,是最常见的性能事故。十个各需 100ms 的请求被串成 1 秒。独立操作用 Promise.all 包起来,有依赖关系才逐个 await。
我的实践中遵循三条:对外接口层尽量 async/await,配合 try/catch 让错误路径显式;库的内部边界可以保留回调形态以避免不必要的包装(Node 核心模块至今双轨提供);永不裸写嵌套超过两层的回调——那说明该抽函数了。
| 维度 | 回调 | Promise | async/await |
|---|---|---|---|
| 可读性 | 嵌套增长 | 链式但密集 | 接近同步 |
| 错误处理 | 每层手工 | catch 收口 | try/catch |
| 并行表达 | 需库辅助 | all/race 原生 | 需主动配合 all |
| 调试栈 | 深且乱 | 较好 | 最好 |
异步代码的可测性差异也值得提前留意。回调风格难以断言——你无法在测试里直接拿到「将来才执行的回调结果」,只能借助 done 参数或替身库;Promise 化之后,测试只需要返回这个 Promise,断言链自然成立;async 函数更进一步,测试本身也写成 async,生产与测试的形态完全一致。很多团队重构成 async 的隐性收益,就是测试代码从「等待与计数」的黑魔法变成了线性的自描文档。