Part 2 Promise 全面掌握(从"会用"到"用对")
🎯 目标
不是会写:
javascript
new Promise(...)
而是:
-
能判断 Promise 是串行还是并行
-
能看懂 Promise 链执行顺序
-
不写出隐形串行代码
-
能设计任务依赖关系
一句话:
Promise 是"未来关系的管理器"
一、Promise 本质再确认
Promise 是一个对象,代表:
一个未来一定会完成或失败的结果
三种状态:
-
pending
-
fulfilled
-
rejected
两个铁律:
-
状态只能改变一次
-
一旦改变不可逆
| 英文 | 推荐中文理解 | 本质含义 |
|---|---|---|
| pending | 未决 | 还没出结果 |
| fulfilled | 已兑现 | 成功了 |
| rejected | 已拒绝 | 失败了 |
再补一个关键点(很多人忽略)
状态转换只能这样:
pending → fulfilled
pending → rejected
不能:
fulfilled → rejected ❌
rejected → fulfilled ❌
一旦确定,终身不变。
二、then 链的核心机制
javascript
readFileP(a)
.then(data => processP(data))
.then(result => writeFileP(b, result))
.catch(err => handle(err))
关键理解:
then 一定返回一个新的 Promise
规则:
-
return 普通值 → 自动 Promise.resolve
-
throw → 自动 Promise.reject
-
return Promise → 接管后续链条
这就是为什么:
-
不需要嵌套
-
不需要层层回调
| 方法 | 状态 | 用途 |
|---|---|---|
| Promise.resolve(value) | fulfilled | 包装成功结果 |
| Promise.reject(error) | rejected | 主动制造失败 |
三、Promise 的错误传播机制
javascript
readFileP(a)
.then(() => {
throw new Error('boom')
})
.catch(err => {
console.log(err)
})
理解:
then 里的 throw 等价于 return Promise.reject()
错误会沿着 then 链向下传递,直到被 catch 捕获。
这和同步 throw 不一样。
举个例子
javascript
function testPromiseError() {
console.log('1. start')
Promise.resolve()
.then(() => {
console.log('2. inside then')
throw new Error('boom')
})
.then(() => {
console.log('3. will not run')
})
.catch(err => {
console.log('4. caught:', err.message)
})
console.log('5. end')
}
testPromiseError()
输出顺序
bash
1. start
5. end
2. inside then
4. caught: boom
1.同步代码先执行
bash
1
5
2.throw 之后的 then 被跳过
这句不会执行:
javascript
console.log('3. will not run')
因为:
throw 会把当前 Promise 变成 rejected
然后直接跳到最近的 catch
四、Promise 并发控制(最关键)
1️⃣ Promise.all
特点:
-
并行执行
-
任意失败 → 整体失败
-
返回数组(顺序不变)
javascript
await Promise.all([p1, p2, p3])
适合:
-
任务互不依赖
-
成败同生共死
⚠️ 常见错误:
javascript
await Promise.all([
await p1,
await p2
])
这会退化成串行。
2️⃣ Promise.allSettled
-
所有任务跑完
-
不管成功失败
-
返回状态数组
适合:
-
批处理
-
日志扫描
-
允许部分失败
3️⃣ Promise.race
-
谁先结束用谁
-
常用于超时控制
4️⃣ Promise.any
-
第一个成功的结果
-
全失败才 reject
五、Promise 执行顺序基础(为事件循环铺垫)
javascript
console.log('1')
Promise.resolve().then(() => {
console.log('2')
})
console.log('3')
输出:
bash
1
3
2
原因:
-
then 属于微任务
-
当前同步代码执行完后才执行
六、必须掌握的心法
-
Promise 不改变异步本质
-
then 只是"未来回调的升级版"
-
并发取决于 Promise 创建时机
-
await 写在哪,性能差在哪
七、实战练习
练习 1:
-
并行读取 3 个文件
-
任意失败 → 整体失败
-
打印耗时
参考答案
创建js practice1.js 同路径分别添加三个文本文件 1.txt 2.txt 3.txt
javascript
const fs = require('fs/promises')
const path = require('path')
async function main() {
const start = Date.now()
try {
// 1️⃣ 先创建 Promise(关键:不要立刻 await)
const p1 = fs.readFile(path.join(__dirname, '1.txt'), 'utf-8')
const p2 = fs.readFile(path.join(__dirname, '2.txt'), 'utf-8')
const p3 = fs.readFile(path.join(__dirname, '3.txt'), 'utf-8')
// 2️⃣ 并行等待
const [file1, file2, file3] = await Promise.all([p1, p2, p3])
console.log('===== file1 =====')
console.log(file1)
console.log('===== file2 =====')
console.log(file2)
console.log('===== file3 =====')
console.log(file3)
const end = Date.now()
console.log('\n总耗时:', end - start, 'ms')
} catch (err) {
console.error('读取失败:', err.message)
}
}
main()
返回结果
bash
===== file1 =====
。。。。
===== file2 =====
。。。
===== file3 =====
。。
总耗时: 5 ms
可把其中一个文件名改错,比如:
javascript
const p3 = fs.readFile(path.join(__dirname, '3333.txt'), 'utf-8')
则结果为
bash
读取失败: ENOENT: no such file or directory, open '文件路径'
练习 2:
-
使用 allSettled
-
输出每个文件成功或失败
参考答案
javascript
const fs = require('fs/promises')
const path = require('path')
async function main() {
const start = Date.now()
try {
// 1️⃣ 先创建 Promise(关键:不要立刻 await)
const p1 = fs.readFile(path.join(__dirname, '1.txt'), 'utf-8')
const p2 = fs.readFile(path.join(__dirname, '2.txt'), 'utf-8')
const p3 = fs.readFile(path.join(__dirname, '3333.txt'), 'utf-8')
// 2️⃣ 并行等待
const [file1, file2, file3] = await Promise.allSettled([p1, p2, p3])
console.log('===== file1 =====')
console.log(file1)
console.log('===== file2 =====')
console.log(file2)
console.log('===== file3 =====')
console.log(file3)
const end = Date.now()
console.log('\n总耗时:', end - start, 'ms')
} catch (err) {
console.error('读取失败:', err.message)
}
}
main()
输出结果
bash
===== file1 =====
{
status: 'fulfilled',
value: '文件内容'
}
===== file2 =====
{
status: 'fulfilled',
value: '文件内容'
}
===== file3 =====
{
status: 'rejected',
reason: Error: ENOENT: no such file or directory, open '文件地址'
at async open (node:internal/fs/promises:639:25)
at async Object.readFile (node:internal/fs/promises:1243:14)
at async Promise.allSettled (index 2)
at async main (报错行位置) {
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: '文件地址'
}
}
总耗时: 6 ms
练习 3:
实现:
javascript
withTimeout(promise, ms)
withTimeout(promise, ms) 是什么?
它 不是 Node 内置函数。
它是我们自己封装的一个工具函数,用来:
给一个 Promise 加"超时限制"
意思是:
-
如果 promise 在规定时间内完成 → 返回结果
-
如果超过时间还没完成 → 直接失败
为什么需要它?
现实后端开发中:
-
调数据库
-
调第三方接口
-
调 AI 服务
-
调微服务
都可能 卡住不返回
如果不做超时控制,那你的接口就会一直挂着。
参考答案
javascript
function slowTask(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve('任务完成')
}, ms)
})
}
function withTimeout(promise, ms) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'))
}, ms)
})
return Promise.race([promise, timeoutPromise])
}
async function main() {
try {
const result = await withTimeout(slowTask(3000), 2000)
console.log(result)
} catch (err) {
console.error('错误:', err.message)
}
}
main()
输出结果:错误: 请求超时
javascript
async function main() {
try {
const result = await withTimeout(slowTask(1000), 2000)
console.log(result)
} catch (err) {
console.error('错误:', err.message)
}
}
输出结果:任务完成
时间线:
bash
0s → 两个同时开始
2s → timeout 先 reject
3s → slowTask 才 resolve(但已经没用了)
⚠️ 注意:
即使 timeout 先返回,slowTask 仍然在后台运行。
这就是 race 的特点。
在真实服务器中:
你不仅要 timeout
你还要:
-
取消数据库查询
-
中断 HTTP 请求
-
释放资源
如果 slowTask 永远不 resolve 也不 reject,会发生什么?
程序会永远卡住在这里
因为:
-
Promise 永远是 pending
-
await 会一直等
-
后面的代码永远不会执行
在服务器里:
-
请求不会返回
-
连接不会关闭
-
资源被占用
-
内存泄漏风险
这就是所谓的:
悬空 Promise(Hanging Promise)
如果一个 Promise 永远 pending:
-
await 会永远卡住
-
then 永远不会执行
-
catch 也不会执行
-
程序会挂在那
Promise 可以取消吗?
标准答案:
原生 Promise 不支持取消,只能通过额外机制(如 AbortController)实现。
Part 3 事件循环基础结构
事件循环的简化模型:
bash
执行同步代码
↓
清空微任务队列
↓
执行一个宏任务
↓
清空微任务队列
↓
循环
一、宏任务 vs 微任务(重点)
🟦 宏任务(Macrotask)
常见:
-
setTimeout
-
setInterval
-
setImmediate
-
I/O
-
整个 script
在 I/O 回调里,setImmediate 会比 setTimeout 更早执行。
🟩 微任务(Microtask)
常见:
-
Promise.then
-
catch
-
finally
-
async/await 后续代码
-
queueMicrotask
Node 还有一个特殊:
- process.nextTick(优先级更高)
记住核心规则:
同步 → 微任务 → 宏任务
列子1
javascript
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
})
console.log('4')
输出
bash
1
4
3
2
原因:
-
1 和 4 是同步
-
Promise.then 是微任务
-
setTimeout 是宏任务
例子2
javascript
Promise.resolve()
.then(() => {
console.log('then1')
})
.then(() => {
console.log('then2')
})
setTimeout(() => console.log('timeout'))
console.log('end')
输出
bash
end
then1
then2
timeout
关键理解:
微任务队列会一次性清空
二、async / await 执行机制
列子
javascript
async function test() {
console.log('1')
await Promise.resolve()
console.log('2')
}
test()
console.log('3')
输出
bash
1
3
2
解释:
-
await 后面的代码被放入微任务队列
-
相当于 Promise.then
三、Node 特有 ------ process.nextTick
优先级
bash
同步
↓
process.nextTick
↓
Promise 微任务
↓
宏任务
例子
javascript
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('promise'))
console.log('sync')
输出
bash
sync
nextTick
promise