Part 1 核心概念
一、同步 vs 异步到底差在哪
1️⃣ 同步(Synchronous)
特点:排队,一个一个来,前面不完事,后面全等着
javascript
const data = fs.readFileSync('a.txt')
console.log(data)
console.log('done')
bash
...文件内容
done
执行顺序是铁板一块:
-
读文件(阻塞)
-
打印 data
-
打印 done
📌 问题不在慢,在"卡"
-
CPU 空着
-
线程被占死
-
不能处理别的请求
2️⃣ 异步(Asynchronous)
特点:我先登记任务,你忙你的,完事我通知你
javascript
fs.readFile('a.txt', (err, data) => {
console.log(data)
})
console.log('done')
bash
done
...文件内容
执行顺序:
-
把"读文件任务"交出去
-
立刻 执行
console.log('done') -
文件读完 → 回调被调用
📌 重点:
异步 ≠ 并行
异步 = 不等结果,先干别的
二、回调地狱为什么会出现?
本质原因只有一句话:
异步操作的"下一步逻辑",只能写在回调里
假设你要做三件事:
-
读文件
-
处理内容
-
再写文件
javascript
readFile(a, (err, data) => {
process(data, (err, result) => {
writeFile(b, result, (err) => {
console.log('done')
})
})
})
回调地狱的 3 大问题:
-
代码向右偏移,阅读成本爆炸
-
错误处理分散(每一层都要 if err)
-
逻辑顺序不直观
📌 关键点:
不是"回调不好",而是回调不适合表达"线性流程"
三、Promise 解决了什么问题?
Promise 的本质(非常重要):
Promise 是一个"对未来结果的占位符"
它干了 3 件关键的事:
1️⃣ 把"未来的结果"变成一个对象
javascript
const p = readFilePromise('a.txt')
这个 p:
-
现在没结果
-
但将来一定有一个结果 or 错误
-
状态只会走一次:
pending → fulfilled / rejected
2️⃣ 把"嵌套"变成"链式"
javascript
readFilePromise(a)
.then(process)
.then(result => writeFilePromise(b, result))
.catch(err => console.error(err))
📌 本质变化:
-
不再"向右写"
-
改为"向下读"
3️⃣ 把错误处理集中化
javascript
.then(...)
.then(...)
.catch(...)
👉 中间任何一步出错,直接跳到 catch
一句话总结 Promise
Promise 把"回调何时执行"这件事,从你脑子里拿走了
四、async / await 的本质
先说结论(很重要):
async / await ≠ 同步执行
它只是把 Promise 写得像同步
1️⃣ async 函数的真实身份
javascript
async function foo() {
return 123
}
等价于:
javascript
function foo() {
return Promise.resolve(123)
}
📌 async 函数一定返回 Promise
2️⃣ await 到底在等什么?
javascript
const data = await readFilePromise('a.txt')
本质是:
-
暂停当前 async 函数
-
不阻塞线程
-
Promise resolved 后,从这里继续执行
📌 暂停的是:
函数执行权,不是线程
3️⃣ try / catch 能抓异步错误的原因
javascript
try {
await readFilePromise('a.txt')
} catch (e) {
console.error(e)
}
因为:
-
await会把 rejected 的 Promise -
直接当成 throw
async / await 的真正价值
让异步逻辑按"人类的思维顺序"写
而不是改变执行模型。
五、那句关键的话,真正想表达什么?
Node.js 用单线程,靠异步把并发"骗"出来
我们拆开看:
1️⃣ Node.js 真的是单线程吗?
-
JS 执行是单线程
-
但:
-
I/O 在系统线程池
-
网络交给操作系统
-
libuv 在背后调度
-
👉 你写的是单线程代码,底层不是
2️⃣ 并发是怎么"骗"出来的?
假设:
-
同时 1000 个请求
-
每个请求都要读数据库
传统同步模型:
-
一个线程卡一个请求
-
线程数 = 并发数
-
爆内存
Node.js:
-
所有请求共用一个 JS 线程
-
I/O 全部异步丢出去
-
完事再通知回来
📌 等待 I/O 时,线程不闲着
3️⃣ 为什么这招特别适合 Web?
Web 请求的特点:
-
90% 时间在等 I/O
-
真正算逻辑的时间很短
👉 Node.js 把"等"的时间榨干了
六、一句话终极总结
-
同步:人等机器
-
异步:机器等人
-
回调:能跑,但难维护
-
Promise:管理未来
-
async/await:管理人类心智负担
-
Node.js:把等待变成武器
七、通过第二回调地狱的列子结合后面的第三和第四分析
javascript
readFile(a, (err, data) => {
process(data, (err, result) => {
writeFile(b, result, (err) => {
console.log('done')
})
})
})
为什么"下一步逻辑只能写在回调里"?
先忘掉 JS,想一件更生活化的事。
场景类比
你让同事帮你做一件事:
"你把文件读完,再告诉我内容,我好继续处理。"
问题是:
👉 你不知道他什么时候读完
所以你只能说:
"你读完了,来敲我一下。"
这个"敲我一下",就是 callback。
在代码里发生了什么?
javascript
readFile(a, callback)
这句代码真正的含义是:
-
我现在 拿不到 data
-
data 未来才会出现
-
那我只能把"用 data 做什么"提前交出去
📌 所以:
回调不是风格选择,而是时间问题逼出来的结构
为什么一有"多步异步",就必然嵌套?
这个例子,其实是一个严格的因果链:
-
process 必须等 readFile
-
writeFile 必须等 process
时间线是这样的:
bash
readFile 完成
↓
process 完成
↓
writeFile 完成
而在回调模型里,唯一能确定"上一步已完成"的地方,只有一个:
上一步的回调函数里
于是结构自然变成:
javascript
readFile(() => {
process(() => {
writeFile(() => {})
})
})
📌 重点:
不是你想嵌套
是"完成时机"逼你嵌套
代码为什么会"向右生长"?
这是个结构性问题,不是格式问题
回调的代码结构是:
javascript
A(() => {
B(() => {
C(() => {
D()
})
})
})
你无法把它写成:
javascript
A()
B()
C()
D()
因为:
-
A 返回时,B 需要的数据还不存在
-
B 根本没资格执行
📌 所以缩进不是"坏习惯",而是时间嵌套的视觉化
错误处理为什么会碎一地?
同步代码的错误流
javascript
try {
A()
B()
C()
} catch (e) {
handle(e)
}
原因很简单:
- 错误沿着调用栈向上冒泡
回调世界的问题
javascript
readFile((err, data) => {
if (err) return handle(err)
process(data, (err, result) => {
if (err) return handle(err)
writeFile(result, (err) => {
if (err) return handle(err)
})
})
})
为什么不能统一 catch?
📌 因为:
回调执行时,最初的调用栈早就没了
错误根本"爬不回去"。
最被忽视的一个问题:逻辑被"打散"了
你脑子里的逻辑是:
读 → 处理 → 写 → 完成
但回调代码长这样:
-
读文件:第 1 行
-
处理逻辑:第 5 行
-
写文件:第 9 行
-
完成:第 12 行
📌 逻辑顺序 ≠ 代码顺序
你在读代码时,必须:
-
跟着缩进跳来跳去
-
在脑子里重建时间线
这就是为什么:
回调地狱消耗的不是 CPU,是人的工作记忆
为什么说"回调不适合表达线性流程"?
回调最适合的场景其实是:
-
事件监听
-
一次性通知
-
不关心顺序的异步结果
比如:
javascript
socket.on('data', data => {})
button.onclick = () => {}
📌 这里没有"下一步必须依赖上一步"的线性关系。
但你的例子是:
-
有顺序
-
有依赖
-
有失败中断
这本质是:
一个异步的"流程"
而回调:
-
没有流程控制能力
-
只有"发生时通知"
一句话压轴
回调的问题不是"写法丑"
而是它把"时间"塞进了"结构"里
Promise / async-await 做的唯一一件事就是:
把"时间关系"从代码结构里解放出来
八、将第二回调地狱例子 一步步重构成 Promise 再重构成 async/await
第一步:原始形态(回调地狱)
javascript
readFile(a, (err, data) => {
if (err) return handle(err)
process(data, (err, result) => {
if (err) return handle(err)
writeFile(b, result, (err) => {
if (err) return handle(err)
console.log('done')
})
})
})
执行时间线(脑补)
bash
readFile 开始
readFile 完成 → 进入回调
process 开始
process 完成 → 进入回调
writeFile 开始
writeFile 完成 → 进入回调
done
📌 时间线清楚,但代码结构 = 时间嵌套
第二步:Promise 化(时间不再靠缩进表达)
1️⃣ 先把每一步变成 Promise
javascript
function readFileP(a) {
return new Promise((resolve, reject) => {
readFile(a, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
function processP(data) {
return new Promise((resolve, reject) => {
process(data, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
function writeFileP(b, result) {
return new Promise((resolve, reject) => {
writeFile(b, result, err => {
if (err) reject(err)
else resolve()
})
})
}
📌 这一刀的本质是:
把"完成时通知"升级成"状态对象"
2️⃣ 用 then 串流程
javascript
readFileP(a)
.then(data => processP(data))
.then(result => writeFileP(b, result))
.then(() => {
console.log('done')
})
.catch(err => handle(err))
完整版
javascript
function readFileP(a) {
return new Promise((resolve, reject) => {
readFile(a, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
function processP(data) {
return new Promise((resolve, reject) => {
process(data, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
function writeFileP(b, result) {
return new Promise((resolve, reject) => {
writeFile(b, result, err => {
if (err) reject(err)
else resolve()
})
})
}
readFileP(a)
.then(data => processP(data))
.then(result => writeFileP(b, result))
.then(() => {
console.log('done')
})
.catch(err => handle(err))
发生了什么?
-
没有嵌套
-
错误集中
-
流程顺序一眼可见
📌 但你可能已经感觉到了:
可读性提升了,但 still 有点"函数味"
第三步:async / await
javascript
async function main() {
try {
const data = await readFileP(a)
const result = await processP(data)
await writeFileP(b, result)
console.log('done')
} catch (err) {
handle(err)
}
}
main()
现在你的代码长得像什么?
👉 同步代码
但我们再强调一次:
执行模型完全没变
第四步:执行模型对比
async / await 实际发生的是:
javascript
const p1 = readFileP(a)
p1.then(data => {
const p2 = processP(data)
p2.then(result => {
const p3 = writeFileP(b, result)
p3.then(() => {
console.log('done')
})
})
})
📌 await 只是把 then 藏起来了
第五步:为什么 async/await 能"治愈"回调地狱?
回调的问题
-
时间关系 = 代码结构
-
控制流分散在回调函数里
async/await 的改变
-
时间关系 = Promise 状态
-
控制流 = 语言级别的顺序语义
换句话说:
回调:
👉「你完成了,我再告诉你我要干嘛」
Promise / await:👉「你先干,我在这等着」
第六步:最重要的一刀
async/await 解决的是:
❌ 异步
❌ 并发
❌ 性能
它真正解决的是:
✅ 人脑的栈深限制
你不再需要在脑子里模拟:
-
哪一层回调
-
哪一层返回
-
哪一层报错
第七步:送你一句工程级总结
回调是"事件通知模型"
Promise 是"状态机"
async/await 是"状态机的顺序视图"