第 3 章:异步编程深入
异步是 Node 的灵魂。这一章梳理回调 → Promise → async/await 的演进脉络,再讲事件机制和错误处理,让你写出干净、可控的异步代码。
3.1 回调函数:一切的起点
Node 最早的异步写法是回调函数——I/O 完成后调用你传入的函数:
javascript
const fs = require('fs');
// 读文件:回调接收 (err, data)
fs.readFile('/etc/hosts', 'utf8', (err, data) => {
if (err) { console.error('读失败', err); return; }
console.log(data);
});问题:多个异步操作串行时,嵌套越来越深,就是著名的「回调地狱」:
javascript
fs.readFile('a.txt', (err, a) => {
fs.readFile('b.txt', (err, b) => {
fs.readFile('c.txt', (err, c) => {
// 三层嵌套,再往下就没法看了
});
});
});3.2 Promise:把回调「拉平」
Promise 是一个「未来才会完成」的对象,有三种状态,状态一旦改变就不可逆:
┌───────── pending(进行中)─────────┐
│ │ │
▼ ▼ ▼
fulfilled(成功) rejected(失败) (终态,不可再变)
.then() .catch()用 Promise 改写上面的链式读取:
javascript
const fs = require('fs/promises');
fs.readFile('a.txt', 'utf8')
.then(a => fs.readFile('b.txt', 'utf8')) // 返回新 Promise,链式串联
.then(b => fs.readFile('c.txt', 'utf8'))
.then(c => console.log(c))
.catch(err => console.error('出错', err)); // 一处捕获所有错误关键规则:
.then()永远返回一个新 Promise,所以能一直.then().then()链下去;- 链上任何一环 reject,都会跳到最近的
.catch()。
3.3 async/await:异步写起来像同步
async/await 是 Promise 的语法糖,让异步代码看起来像同步,可读性最好:
javascript
const fs = require('fs/promises');
async function readAll() {
try {
const a = await fs.readFile('a.txt', 'utf8'); // 等待,不阻塞事件循环
const b = await fs.readFile('b.txt', 'utf8');
const c = await fs.readFile('c.txt', 'utf8');
return a + b + c;
} catch (err) {
console.error('出错', err);
}
}注意一个常见误区:await 是「暂停当前 async 函数」,不是「阻塞整个线程」——await 期间事件循环继续处理其他任务。
演进对比一览:
回调 嵌套深、难读、易漏错 ──► 早期
Promise 拉平嵌套、统一错误 ──► 主流
async/await 最像同步、最易读 ──► 现代首选实践建议:新代码一律
async/await;Promise.all并行多个独立异步任务;只在万不得已时写回调(如某些老库)。
3.4 并发:Promise.all 并行执行
多个互不依赖的异步任务,用 Promise.all 并行,而不是串行 await:
javascript
// ❌ 串行:总耗时 = 三者之和
const a = await fetchDataA();
const b = await fetchDataB();
const c = await fetchDataC();
// ✅ 并行:总耗时 ≈ 最慢的那个
const [a, b, c] = await Promise.all([
fetchDataA(),
fetchDataB(),
fetchDataC(),
]);串行: A ──► B ──► C (3 个单位时间)
并行: A ─┐
B ─┼──► 汇总 (1 个单位时间)
C ─┘3.5 事件机制:EventEmitter
除了回调/Promise,Node 还有一种异步范式——事件驱动。EventEmitter 让你发布/订阅事件:
javascript
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
// 订阅:监听 'data' 事件
emitter.on('data', (chunk) => {
console.log('收到数据块', chunk);
});
// 发布:触发 'data' 事件(可多次)
emitter.emit('data', 'hello');
emitter.emit('data', 'world');典型应用:http.Server、fs.ReadStream 都是 EventEmitter,所以能 server.on('request', ...)、stream.on('data', ...)。
on 和 once 的区别:on 每次事件都触发,once 只触发一次。
3.6 错误处理三原则
异步错误处理最容易漏,记住三条:
- Promise 链末尾一定有
.catch,或async函数里try/catch; - 回调风格:约定
err作为第一个参数,先判断if (err); async函数:它永远返回 Promise,调用方要用await或.catch,否则错误会静默丢失。
javascript
// ❌ 危险:错误没人接,变成 unhandled rejection
someAsyncTask();
// ✅ 安全
someAsyncTask().catch(err => console.error(err));3.7 本章小结
| 要点 | 说明 |
|---|---|
| 演进脉络 | 回调(易嵌套)→ Promise(拉平)→ async/await(首选) |
| Promise 状态 | pending → fulfilled/rejected,不可逆 |
| 并行 | 独立任务用 Promise.all 并行,别串行 await |
| 事件 | EventEmitter 发布/订阅,是 Node 内置对象的基石 |
| 错误处理 | catch 别漏,async 函数要接住 |
下一章预告
异步模型掌握了,该上手 Node 自带的武器库了——fs、path、http、stream 四大核心模块,日常开发绕不开。