异步编程
Node.js 的异步模型是"单线程 + 非阻塞 I/O":一个线程处理所有请求,遇到 I/O 操作(读文件、查数据库、发请求)不干等,而是注册回调后继续处理下一个任务。这是 Node.js 高并发的根源:请求之间靠"事件循环"排队,CPU 永远不会空等慢速的 I/O。代价是异步代码的写法复杂——回调、Promise、async/await 三代演进,都是在降低这种写法的痛苦。
提示
三代演进解决同一个问题——"异步结果怎么拿":回调靠嵌套函数传结果(嵌套地狱),Promise 靠链式调用(.then 链条),async/await 靠同步语法(最接近人的直觉)。新代码统一用 async/await。
回调与回调地狱
回调是最原始的异步写法:I/O 完成后调用传入的函数。读取三个文件需要三层嵌套,这就是"回调地狱":
// 回调地狱:嵌套越深越难维护
fs.readFile('a.json', (err, dataA) => {
fs.readFile('b.json', (err, dataB) => {
fs.readFile('c.json', (err, dataC) => {
// 三个数据在这里才能一起用
});
});
});回调地狱的两个问题:横向膨胀(每多一步多一层缩进)和错误处理混乱(每层都要手写 if (err))。Promise 就是为这两个问题而生的。
Promise
Promise 代表"一个尚未完成但最终会出结果的操作",三种状态流转:
状态一旦改变就不可逆:pending → fulfilled 或 pending → rejected 后不能再变,所以 Promise 天然免疫"回调被调用两次"的竞态。链式调用解决嵌套问题:
const promise = new Promise((resolve, reject) => {
fs.readFile('a.json', (err, data) => {
if (err) reject(err); // 失败 → rejected
else resolve(data); // 成功 → fulfilled
});
});
promise
.then(data => JSON.parse(data)) // 每个 then 返回新 Promise
.then(obj => console.log(obj)) // 可以无限链下去
.catch(err => console.error(err)); // 错误统一汇聚到这里| 静态方法 | 行为 |
|---|---|
Promise.all([...]) | 全部成功才 fulfilled,一个失败立即 rejected(并发,等最慢的) |
Promise.allSettled([...]) | 等所有结束,各自返回成功/失败结果(不因单个失败中断) |
Promise.race([...]) | 第一个出结果的胜出(超时控制的常用手段) |
Promise.resolve(v) | 把普通值包装成已成功的 Promise |
并发与串行的选择
多个独立异步任务用 Promise.all 并发执行(总耗时 = 最慢的那个);有依赖关系的任务(B 需要 A 的结果)必须串行链式调用。这是 Node.js 性能优化的第一课。
async/await
async/await 是 Promise 的语法糖:async 函数总是返回 Promise,await 让异步代码看起来像同步代码:
async function fetchData() {
try {
const res = await fetch('https://api.example.com'); // 像同步一样取值
const data = await res.json();
return data;
} catch (err) { // 用 try/catch 捕获,不再需要 .catch
console.error(err);
}
}async/await 的最大价值是让"顺序逻辑"可读:三层依赖的读取变成三行顺序语句,错误处理收敛到一层 try/catch,与同步代码的写法完全对齐。
三代写法对比
同一个"读三个文件再合并"的任务,三种写法的结构差异一目了然:
fs.readFile('a', (e1, a) => {
fs.readFile('b', (e2, b) => {
fs.readFile('c', (e3, c) => {
use(a, b, c);
});
});
});const read = f => new Promise((res, rej) =>
fs.readFile(f, (e, d) => e ? rej(e) : res(d)));
Promise.all([read('a'), read('b'), read('c')])
.then(([a, b, c]) => use(a, b, c))
.catch(err => console.error(err));async function main() {
try {
const [a, b, c] = await Promise.all([
fs.promises.readFile('a'),
fs.promises.readFile('b'),
fs.promises.readFile('c'),
]);
use(a, b, c);
} catch (err) { console.error(err); }
}并发控制
多个异步任务还需要细粒度的流程控制——串行、并行、或"下一个依赖上一个":
| 库/方法 | 场景 |
|---|---|
Promise.all | 并行执行互不依赖的任务,等全部完成 |
| eventproxy | 事件式并发:多个请求完成后统一回调(ep.all('e1','e2', cb)) |
| async 库 | 传统流程控制:series 串行、parallel 并行、waterfall 前一个结果传给下一个 |
const async = require('async');
// waterfall:上一个任务的输出自动成为下一个任务的输入
async.waterfall([
(callback) => { callback(null, 'step1'); },
(prev, callback) => { callback(null, prev + ' → step2'); },
], (err, result) => console.log(result));错误处理
同步错误、回调错误、async/await 错误的捕获方式各不相同:
try {
JSON.parse('invalid json');
} catch (err) {
console.error(err); // 同步异常用 try/catch
}asyncOperation((err, result) => {
if (err) { // 回调风格:err 参数约定
console.error(err);
return;
}
});try {
const result = await asyncOperation();
} catch (err) {
console.error(err); // await 的异常会抛出,可被 catch 捕获
}未捕获的异步错误
Promise 链末尾必须有 .catch,async 函数必须有 try/catch——否则错误会变成"未处理的 Promise 拒绝",在旧版 Node.js 中直接导致进程崩溃。兜底手段:process.on('unhandledRejection', handler) 全局监听。
异步思想贯穿整个 Node.js 生态——Node.js开发 的事件循环是异步的调度器,npm包管理 的安装过程也是异步的。理解"等待时不阻塞"这个模型,是理解 效率工具 中所有 Node.js 工具的基础。