异步编程模型
本教程共 76 篇 · 第 11 篇 · 更新于 2026-07-25 · 约 4 分钟阅读
11. 异步编程模型
本节目标:回调、Promise、async/await 三种异步写法的演进与选型。
你走进一家快餐店,点完单后站在柜台前死死盯着后厨,直到汉堡做好才肯挪步——这就是阻塞(Blocking)。正常人都会拿小票找座位,等叫号了再去取餐,期间还能刷手机。这就是非阻塞(Non-blocking)。
Node.js 的底层设计,就是让你始终做后者。
单线程与并发
JavaScript 在 Node.js 里只有一个执行线程。听起来很弱?其实它借助操作系统内核和 libuv,把耗时的 I/O(文件读写、网络请求、DNS 查询)统统“外包”出去。等内核干完活,再通过回调通知你的代码。于是单线程就能同时服务成千上万个连接。
NoteNode.js 的“单线程”指的是你的 JavaScript 代码运行在一个线程里。底层 libuv 会动用线程池处理部分 I/O,这对你来说是透明的。
这种模型叫事件驱动、非阻塞 I/O,是 Node.js 高并发的根基。
阻塞 vs 非阻塞:一眼看出差别
用 node:fs 模块做对比最直观。先创建一个 demo.txt,内容随意:
echo "hello from file" > demo.txt
阻塞式读取:
import { readFileSync } from 'node:fs';
const data = readFileSync('demo.txt', 'utf8');
console.log(data);
console.log('后续代码');
readFileSync 会把线程卡住,直到文件读完。文件越大,卡得越久。
非阻塞式读取:
import { readFile } from 'node:fs';
readFile('demo.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取出错:', err);
return;
}
console.log(data);
});
console.log('后续代码');
运行顺序是:
后续代码
hello from file
readFile 把任务丢给操作系统,立刻继续执行后面的代码。等文件读完,回调函数被推进事件循环,最终执行。吞吐量就这么拉开了差距。
Warning混合同步和异步代码是大坑。下面这段看似合理,实际可能删掉文件后才去读:
import { readFile, unlinkSync } from 'node:fs'; readFile('demo.txt', (err, data) => { /* ... */ }); unlinkSync('demo.txt'); // 危险!正确的做法是把
unlink也放进回调,或者用fs/promises。
回调函数:Node.js 的原始异步语法
回调函数就是“等这事完了,调用我”。在 Node.js 里,异步 API 的回调遵循**错误优先(error-first)**约定:第一个参数是错误对象,没有错误时为 null。
import { readFile } from 'node:fs';
readFile('demo.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err.message);
return;
}
console.log('内容:', data);
});
这个约定写进了 Node.js 的基因。早期核心模块全是这个风格,所以你也得眼熟它——哪怕现在更推荐使用 Promise 版本。
回调地狱:层叠的噩梦
回调本身没问题,但嵌套多了就恶心。假设你要按顺序读三个文件,再写一个新文件:
import { readFile, writeFile } from 'node:fs';
readFile('a.txt', 'utf8', (err, a) => {
if (err) { console.error(err); return; }
readFile('b.txt', 'utf8', (err, b) => {
if (err) { console.error(err); return; }
readFile('c.txt', 'utf8', (err, c) => {
if (err) { console.error(err); return; }
writeFile('out.txt', a + b + c, (err) => {
if (err) { console.error(err); return; }
console.log('合并完成');
});
});
});
});
代码向右狂奔,错误处理重复到让人发疯。这就是回调地狱(Callback Hell)。我早年写过一个六层嵌套的数据导入脚本,改一个需求要花一下午,因为眼睛根本找不到上下文。
解决思路有两个方向:
- 把函数拆出去,减少匿名回调的嵌套深度。
- 升级到 Promise 或 async/await,从根本上消灭嵌套。
Node.js 核心模块如今都提供了 Promise 版本,位于 node:fs/promises、node:timers/promises 等子路径下。本书后续章节会逐个展开。
并发不是并行
Node.js 文档里常说“并发(Concurrency)”。注意,它不是“并行(Parallelism)”。并发指的是事件循环调度多个任务的能力,同一时刻只有一个 JavaScript 函数在执行。并行才意味着多核同时跑代码,那得靠 worker_threads 或 cluster,后面章节会讲。
对 I/O 密集型应用(Web API、网关、代理),单线程并发模型非常高效。因为大部分时间都在等网络响应,JavaScript 线程可以不停切换去处理别的请求。但如果你的回调里塞了一个大循环,所有人都要陪你等。