从 Promise 基础、手写 Promise,到 Promise/A+、async/await、Generator,再到并发控制与工程实践。
一、目标与学习路线
JavaScript 的异步编程并不是简单地记住几个 API。
真正需要掌握的是:
- Promise 到底解决了什么问题;
- Promise 的状态如何变化;
then()为什么能够链式调用;- Promise 为什么一定是异步执行回调;
async/await和 Promise 到底是什么关系;- Generator 的暂停与恢复是怎么工作的;
- 如何自己实现一个 Promise;
- 如何实现一个 Generator → Async 的执行器;
- 多个异步任务应该如何并发执行;
- 如何限制并发数量;
- 如何处理超时、取消、重试、错误传播等真实项目问题。
因此,本章按照下面的路径学习:
javascript
异步编程基础
↓
Promise 状态与 then
↓
手写简版 Promise
↓
Promise 链式调用
↓
Promise 执行顺序与微任务
↓
Promise 静态方法
↓
并发控制 Scheduler
↓
Promise/A+ 与 Promise Resolution Procedure
↓
async/await
↓
Generator
↓
Generator + Promise
↓
Generator 模拟 async/await
↓
现代异步工程实践
二、Promise:从状态模型到链式调用
Promise 是 JavaScript 异步编程的核心抽象之一。
它解决的并不是"让 JavaScript 变成同步",而是:
把一个未来才会产生结果的异步操作,封装成一个可以继续组合、传递和处理的对象。
一个 Promise 有三种状态:
pending
├──→ fulfilled
│
└──→ rejected
其中:
pending:初始状态,等待结果;fulfilled:异步操作成功;rejected:异步操作失败。
Promise 一旦从 pending 进入 fulfilled 或 rejected,就不能再次改变。
这里还需要特别注意一个容易混淆的概念:
resolved和fulfilled并不完全等价。
例如:
javascript
const p = new Promise(resolve => {
resolve(
new Promise(resolveInner => {
setTimeout(() => resolveInner('success'), 1000)
})
)
})
外层 Promise 很快就进入了"resolved / 锁定到内部 Promise"的状态,但最终 fulfilled 要等内部 Promise 完成。
日常开发中经常把 resolved 当作 fulfilled 使用,但理解 Promise 底层时必须知道两者的区别。
2.1 resolve、reject 与 Promise 状态
先看最基本的例子:
javascript
const p1 = new Promise((resolve, reject) => {
resolve('success')
reject('fail')
})
console.log(p1)
最终结果是:
javascript
Promise { 'success' }
因为第一次状态转换已经发生。
再看:
javascript
const p2 = new Promise((resolve, reject) => {
reject('fail')
resolve('success')
})
最终:
css
Promise { <rejected>: 'fail' }
因此可以总结:
- 执行
resolve(),Promise 可以进入 fulfilled; - 执行
reject(),Promise 可以进入 rejected; - Promise 状态只能从 pending 进入 settled 状态;
- 一旦 settled,后续的
resolve()/reject()不再改变它; - executor 中同步抛出的异常,会使 Promise rejected。
例如:
javascript
const p3 = new Promise(() => {
throw new Error('error')
})
console.log(p3)
相当于:
javascript
const p3 = new Promise((resolve, reject) => {
reject(new Error('error'))
})
2.2 手写 Promise:先实现状态和结果
为了理解 Promise,我们先实现一个简版。
kotlin
class MyPromise {
constructor(executor) {
this.initValue()
this.initBind()
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
initValue() {
this.PromiseResult = undefined
this.PromiseState = 'pending'
}
initBind() {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
resolve(value) {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'fulfilled'
this.PromiseResult = value
}
reject(reason) {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'rejected'
this.PromiseResult = reason
}
}
这里有三个值得重点理解的地方。
第一:为什么要初始化为 pending?
因为 Promise 创建出来的时候,还不知道最终结果:
scss
new MyPromise(...)
↓
pending
↓
┌─────┴─────┐
↓ ↓
fulfilled rejected
第二:为什么 resolve/reject 要绑定 this?
因为:
kotlin
executor(this.resolve, this.reject)
这里传递的是函数引用。
如果没有绑定:
kotlin
this.resolve = this.resolve.bind(this)
那么函数脱离实例调用后:
scss
resolve('success')
内部的 this 就可能不再指向当前 Promise 实例。
第三:为什么要判断 pending?
因为:
scss
resolve('success')
reject('fail')
只能让第一次状态转换生效。
所以:
kotlin
if (this.PromiseState !== 'pending') return
是 Promise 状态不可逆的核心。
2.3 executor 中的 throw
Promise 构造函数执行 executor 时,本质上需要捕获同步异常:
kotlin
class MyPromise {
constructor(executor) {
this.initValue()
this.initBind()
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
// ...
}
例如:
javascript
const p = new MyPromise(() => {
throw new Error('fail')
})
console.log(p.PromiseState)
// rejected
但这里需要注意:
只能捕获 executor 执行阶段的同步异常。
例如:
javascript
new Promise(() => {
setTimeout(() => {
throw new Error('error')
}, 1000)
})
这个异常发生在定时器回调中,并不是 Promise constructor 当前调用栈中的同步异常,不能简单理解为会被 constructor 外层的 try/catch 自动捕获。
这也是理解 JavaScript 异步错误处理非常重要的一步。
2.4 then:Promise 真正开始发挥作用
Promise 最核心的方法是:
arduino
promise.then(onFulfilled, onRejected)
例如:
javascript
const p1 = new Promise((resolve, reject) => {
resolve('success')
})
p1.then(
res => console.log(res),
err => console.log(err)
)
then() 接收两个参数:
scss
then(
onFulfilled,
onRejected
)
分别处理:
fulfilled → onFulfilled
rejected → onRejected
例如:
javascript
const p1 = new Promise(resolve => {
resolve('success')
})
p1.then(
res => console.log(res),
err => console.log(err)
)
输出:
success
如果:
javascript
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('fail')
}, 1000)
})
p2.then(
res => console.log(res),
err => console.log(err)
)
则一秒后执行失败回调。
2.5 pending 时为什么要保存回调?
这是手写 Promise 时最重要的一步。
考虑:
javascript
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
p.then(
value => console.log(value),
reason => console.log(reason)
)
执行:
css
p.then(...)
时,Promise 还是:
pending
此时不能立即执行成功回调。
但是不能把回调直接丢掉。
所以需要保存:
ini
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
完整实现:
kotlin
class MyPromise {
constructor(executor) {
this.initValue()
this.initBind()
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
initValue() {
this.PromiseResult = undefined
this.PromiseState = 'pending'
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
}
initBind() {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
resolve(value) {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'fulfilled'
this.PromiseResult = value
while (this.onFulfilledCallbacks.length) {
const callback = this.onFulfilledCallbacks.shift()
callback(value)
}
}
reject(reason) {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'rejected'
this.PromiseResult = reason
while (this.onRejectedCallbacks.length) {
const callback = this.onRejectedCallbacks.shift()
callback(reason)
}
}
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === 'function'
? onFulfilled
: value => value
onRejected =
typeof onRejected === 'function'
? onRejected
: reason => {
throw reason
}
if (this.PromiseState === 'fulfilled') {
onFulfilled(this.PromiseResult)
}
if (this.PromiseState === 'rejected') {
onRejected(this.PromiseResult)
}
if (this.PromiseState === 'pending') {
this.onFulfilledCallbacks.push(onFulfilled)
this.onRejectedCallbacks.push(onRejected)
}
}
}
这里体现出 Promise 的一个核心设计:
Promise 可以先完成,再注册 then;也可以先注册 then,再完成。两种情况下最终都能执行对应回调。
因此不存在:
"我注册晚了,所以错过结果"
这样的竞争问题。
三、Promise 链式调用与执行机制
前面的 Promise 还不完整。
因为真正的 Promise 最大的价值之一,就是:
scss
promise
.then(...)
.then(...)
.then(...)
3.1 为什么 then 必须返回一个新的 Promise?
看下面代码:
ini
const p = Promise.resolve(100)
p
.then(res => res * 2)
.then(res => console.log(res))
输出:
200
这里意味着:
css
p.then(...)
本身必须返回一个新的 Promise。
可以理解成:
promise1
↓ then
promise2
↓ then
promise3
所以:
ini
const promise2 = promise1.then(...)
是 Promise 链式调用的基础。
3.2 then 返回普通值
例如:
javascript
Promise.resolve(100)
.then(res => {
return res * 2
})
.then(res => {
console.log(res)
})
输出:
200
因为:
kotlin
return 200
会让下一个 Promise fulfilled:
java
promise1
↓
then
↓
return 200
↓
promise2 fulfilled(200)
3.3 then 返回 Promise
例如:
javascript
Promise.resolve(100)
.then(res => {
return new Promise(resolve => {
resolve(res * 3)
})
})
.then(res => {
console.log(res)
})
最终:
300
此时第二个 Promise 会等待返回的 Promise。
因此:
arduino
return promise
与:
kotlin
return value
的处理方式不同。
3.4 then 的返回值实际上遵循 Promise Resolution Procedure
简化理解:
ini
const x = onFulfilled(value)
然后:
javascript
x 是普通值
↓
新 Promise fulfilled(x)
x 是 Promise / thenable
↓
新 Promise 跟随 x 的最终状态
onFulfilled 抛异常
↓
新 Promise rejected(error)
这就是 Promise 链式调用真正的核心。
3.5 手写链式调用
javascript
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === 'function'
? onFulfilled
: value => value
onRejected =
typeof onRejected === 'function'
? onRejected
: reason => {
throw reason
}
const thenPromise = new MyPromise((resolve, reject) => {
const resolvePromise = callback => {
try {
const x = callback(this.PromiseResult)
if (x === thenPromise) {
throw new TypeError('Chaining cycle detected')
}
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else {
resolve(x)
}
} catch (error) {
reject(error)
}
}
if (this.PromiseState === 'fulfilled') {
resolvePromise(onFulfilled)
}
if (this.PromiseState === 'rejected') {
resolvePromise(onRejected)
}
if (this.PromiseState === 'pending') {
this.onFulfilledCallbacks.push(
() => resolvePromise(onFulfilled)
)
this.onRejectedCallbacks.push(
() => resolvePromise(onRejected)
)
}
})
return thenPromise
}
测试:
ini
const test = new MyPromise(resolve => {
resolve(100)
})
test
.then(res => res * 2)
.then(res => console.log(res))
输出:
200
3.6 这里的 instanceof Promise 有什么问题?
上面的代码:
java
if (x instanceof MyPromise)
只是教学版实现。
真正的 Promise 并不是简单判断:
javascript
x instanceof Promise
因为 Promise 需要支持 thenable assimilation。
例如:
scss
const thenable = {
then(resolve) {
resolve(100)
}
}
Promise.resolve(thenable).then(console.log)
最终:
100
所以:
只要一个对象具有符合约定的
then方法,它就可能被 Promise 当作 thenable 处理。
这也是 Promise/A+ 中 [[Resolve]](promise, x) 的核心。
3.7 catch 与 finally
现代 Promise 学习不能只停留在 then()。
catch
vbnet
promise.catch(error => {
console.error(error)
})
本质上相当于:
go
promise.then(undefined, error => {
console.error(error)
})
catch() 自身也会返回新的 Promise,因此依然可以继续链式调用。
典型写法:
scss
request()
.then(handleData)
.then(processData)
.catch(handleError)
这样可以统一处理整个 Promise 链中的错误。
finally
finally() 无论成功还是失败都会执行:
dart
request()
.then(data => {
// success
})
.catch(error => {
// error
})
.finally(() => {
// cleanup
})
常用于:
loading 状态关闭
释放资源
隐藏 loading
清理临时状态
例如:
scss
setLoading(true)
request()
.then(handleData)
.catch(handleError)
.finally(() => {
setLoading(false)
})
finally() 返回的仍然是一个新的 Promise;如果 finally 回调抛异常或返回 rejected Promise,后续链也会进入 rejected。
四、Promise 的执行顺序、微任务与静态方法
Promise 学到这里,必须开始理解:
为什么
then()明明写在前面,却不是立即执行?
4.1 then 回调不是同步执行
javascript
const p = new Promise(resolve => {
resolve(1)
})
p.then(res => {
console.log(res)
})
console.log(2)
输出:
2
1
原因不是:
"then 方法本身是一个微任务"。
更准确的说法是:
then()注册的是 Promise reaction;Promise settled 后,对应回调会被安排到 JavaScript 的 Job / Promise reaction 队列中异步执行,因此当前同步代码会先执行完。
也就是说:
javascript
同步代码
↓
当前调用栈执行完成
↓
Promise reaction / microtask
↓
后续任务
Promise 的 handler 会被异步调度,即使 Promise 在注册 then() 之前已经 fulfilled。
4.2 手写 Promise 时模拟异步执行
使用:
scss
setTimeout(...)
来模拟异步执行。
这种写法可以帮助理解"不能同步调用 then 回调",但需要特别说明:
setTimeout是计时器任务,并不等价于 Promise 的微任务调度。
所以教学代码可以写成:
scss
setTimeout(() => {
callback(value)
})
但真正的 Promise 应该使用 Promise Job / microtask 语义。
这也是为什么手写 Promise 过程中:
scss
setTimeout(...)
只能作为简化教学手段,而不能认为它就是 Promise 的真实实现。
4.3 Promise.all
Promise.all() 用于:
多个任务同时启动,并等待全部成功。
scss
const p1 = request1()
const p2 = request2()
const p3 = request3()
Promise.all([p1, p2, p3])
.then(([r1, r2, r3]) => {
console.log(r1, r2, r3)
})
特点:
全部成功 → fulfilled
任意一个失败 → rejected
注意:
Promise.all()reject 并不会自动取消其他已经启动的任务。
例如:
ini
const p1 = request1()
const p2 = request2()
const p3 = request3()
Promise.all([p1, p2, p3])
如果 p1 失败,Promise.all() 很快 rejected,但 p2、p3 本身可能仍然继续执行。
手写核心思路
scss
static all(promises) {
const result = []
let count = 0
return new MyPromise((resolve, reject) => {
if (promises.length === 0) {
resolve([])
return
}
const addData = (index, value) => {
result[index] = value
count++
if (count === promises.length) {
resolve(result)
}
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => addData(index, value),
reject
)
})
})
}
这里还有一个重要点:
css
result[index] = value
而不是:
scss
result.push(value)
因为 Promise 的完成顺序可能不同,但 Promise.all() 的结果顺序必须与输入顺序一致。
4.4 Promise.race
race():
谁先 settled,就使用谁的结果。
scss
Promise.race([
request1(),
request2(),
request3()
])
可能:
scss
request2 先成功
↓
race fulfilled(request2)
也可能:
scss
request1 先失败
↓
race rejected(request1)
所以:
javascript
第一个 fulfilled 或 rejected
↓
Promise.race
↓
结束
同样:
race()只决定返回 Promise 的状态,并不会自动取消其他任务。
4.5 Promise.allSettled
allSettled() 和 all() 的区别是:
css
all
↓
一个失败就整体 rejected
allSettled
↓
所有任务都结束后整体 fulfilled
例如:
javascript
Promise.allSettled([
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
]).then(results => {
console.log(results)
})
结果类似:
lua
[
{
status: 'fulfilled',
value: 1
},
{
status: 'rejected',
reason: 'error'
},
{
status: 'fulfilled',
value: 3
}
]
标准结果是:
css
{
status: 'fulfilled',
value
}
或者:
css
{
status: 'rejected',
reason
}
allSettled() 会等待所有输入 Promise settled,并且结果数组仍然保持输入顺序。
4.6 Promise.any
Promise.any() 与 Promise.all() 的逻辑可以理解为相反方向:
python
all:
全部成功 → 成功
一个失败 → 失败
any:
一个成功 → 成功
全部失败 → 失败
例如:
scss
Promise.any([
request1(),
request2(),
request3()
])
只要其中一个成功:
fulfilled
如果全部失败:
rejected
并且失败原因是:
AggregateError
而不是普通 Error。
4.7 Promise.resolve 与 Promise.reject
现代 Promise 基础还需要掌握两个非常常用的静态方法:
javascript
Promise.resolve(value)
Promise.reject(reason)
例如:
javascript
Promise.resolve(100)
.then(console.log)
以及:
javascript
Promise.reject(new Error('failed'))
.catch(console.error)
尤其需要理解:
javascript
Promise.resolve(thenable)
会尝试"同化"这个 thenable,而不是简单地把对象原样作为 value。
五、Promise 并发控制:从 Scheduler 到真实工程
Promise 解决了异步任务的组合问题,但并没有自动解决:
"一次到底允许跑多少个异步任务?"
例如:
scss
for (const item of list) {
request(item)
}
如果有:
10000 个任务
全部同时启动可能导致:
- 浏览器连接压力;
- 服务端压力;
- 内存占用;
- API 限流;
- 数据库压力;
- 请求失败率增加。
因此需要并发控制。
5.1 Scheduler 问题
要求:
同时运行的任务最多 N 个。
例如:
scss
const timeout = time =>
new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler(2)
const addTask = (time, order) => {
scheduler
.add(() => timeout(time))
.then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
输出:
2
3
1
4
5.2 为什么是 2、3、1、4?
并发限制:
ini
max = 2
开始:
任务1 → 执行
任务2 → 执行
任务3 → 等待
任务4 → 等待
500ms:
任务2完成
↓
任务3开始
800ms:
任务3完成
↓
任务4开始
1000ms:
任务1完成
1200ms:
任务4完成
所以:
2 → 3 → 1 → 4
5.3 Scheduler 的核心数据结构
只需要三个东西:
arduino
max
count
queue
分别表示:
arduino
max → 最大并发数
count → 当前执行中的任务数
queue → 等待执行的任务
核心逻辑:
arduino
add(task)
↓
count >= max ?
├── 是 → 放入 queue 等待
│
└── 否 → count++
↓
执行 task
↓
count--
↓
唤醒 queue 中任务
5.4 基础 Scheduler 实现
kotlin
class Scheduler {
constructor(max) {
this.max = max
this.count = 0
this.queue = []
}
async add(promiseCreator) {
if (this.count >= this.max) {
await new Promise(resolve => {
this.queue.push(resolve)
})
}
this.count++
try {
return await promiseCreator()
} finally {
this.count--
if (this.queue.length) {
this.queue.shift()()
}
}
}
}
这里相比原始版本增加了一个非常重要的改进:
csharp
finally
因为任务可能失败。
原代码:
csharp
let res = await promiseCreator()
this.count--
如果:
scss
promiseCreator()
reject:
arduino
await 抛异常
↓
this.count-- 不执行
↓
Scheduler 永远认为还有一个任务在执行
最终可能造成:
死锁
所以工程代码应该优先保证资源释放:
kotlin
try {
return await promiseCreator()
} finally {
this.count--
}
5.5 并发控制在真实项目中的应用
Scheduler 并不只是面试题。
实际开发中经常需要:
批量上传
批量请求
图片压缩
文件转换
AI API 调用
Embedding
批量 OCR
爬取数据
批量数据库操作
例如:
javascript
const scheduler = new Scheduler(5)
const results = await Promise.all(
urls.map(url =>
scheduler.add(() => fetch(url))
)
)
这里实际上形成了:
diff
Promise.all
+
Concurrency Limit
这是一种非常常见的工程模式。
六、Promise/A+:理解 Promise 链式调用的标准
前面的 MyPromise 是教学版本。
如果想真正理解 Promise,需要继续看:
Promises/A+
官方规范:
Promises/A+ 的重点并不是规定完整的 JavaScript Promise API,而主要规定:
Promise 的
then()应该如何工作,以及不同 Promise 实现之间如何互操作。
因此必须理解:
javascript
Promise
then
thenable
Promise Resolution Procedure
6.1 Promises/A+ 术语
Promise
一个拥有符合规范的 then 方法的对象或函数。
Thenable
只要拥有:
scss
then()
方法,就可以被认为是 thenable。
例如:
scss
const thenable = {
then(resolve) {
resolve(100)
}
}
它不一定是:
javascript
new Promise(...)
但它具有 Promise-like 行为。
value
任何合法 JavaScript 值:
typescript
number
string
object
undefined
Promise
thenable
都可以作为 value。
reason
Promise rejected 时的原因。
exception
通过:
arduino
throw
抛出的异常值。
6.2 Promise 状态要求
Promises/A+ 规定:
pending
fulfilled
rejected
pending:
可以 → fulfilled
可以 → rejected
fulfilled:
不能再改变
必须拥有 value
rejected:
不能再改变
必须拥有 reason
这与前面手写 Promise 的状态模型是一致的。
6.3 then 的规范要求
形式:
arduino
promise.then(onFulfilled, onRejected)
两个参数都是可选的。
如果:
onFulfilled
不是函数:
忽略
如果:
onRejected
不是函数:
忽略
并且:
arduino
promise.then(...)
必须返回一个新的 Promise:
ini
promise2 = promise1.then(...)
这是链式调用的根本。
七、Promise Resolution Procedure:手写 Promise 的真正难点
真正让 Promise 实现变复杂的不是:
pending
fulfilled
rejected
而是:
lua
[[Resolve]](promise, x)
也就是:
当
then()回调返回一个值x时,新 Promise 到底应该处于什么状态?
7.1 返回普通值
javascript
Promise.resolve(1)
.then(() => {
return 100
})
结果:
scss
fulfilled(100)
7.2 返回 Promise
javascript
Promise.resolve(1)
.then(() => {
return Promise.resolve(100)
})
新的 Promise 会跟随返回 Promise。
7.3 返回 rejected Promise
javascript
Promise.resolve(1)
.then(() => {
return Promise.reject('error')
})
.catch(console.log)
最终:
go
error
7.4 返回 thenable
scss
const thenable = {
then(resolve) {
resolve(100)
}
}
Promise.resolve(thenable)
.then(console.log)
输出:
100
这就是 thenable assimilation。
7.5 返回自身形成循环
例如:
javascript
let promise2
promise2 = promise1.then(() => {
return promise2
})
这会形成:
promise2
↓
等待自己
↓
永远无法完成
因此应该 rejected:
javascript
TypeError
Promises/A+ 明确规定了这种自引用情况。
7.6 thenable 的复杂情况
真正的 [[Resolve]] 还需要处理:
scss
const thenable = {
then(resolve, reject) {
resolve(1)
reject(2)
}
}
只能第一次生效:
scss
resolve(1) → 生效
reject(2) → 忽略
以及:
csharp
const thenable = {
get then() {
throw new Error('failed')
}
}
读取:
x.then
本身就可能抛异常。
所以 Promise Resolution Procedure 才是 Promise 实现中最复杂的部分。
7.7 一个更接近规范的 resolvePromise
教学版可以进一步抽象:
javascript
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) {
reject(new TypeError('Chaining cycle detected'))
return
}
if (
x !== null &&
(typeof x === 'object' || typeof x === 'function')
) {
let called = false
try {
const then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return
called = true
resolvePromise(
promise,
y,
resolve,
reject
)
},
reason => {
if (called) return
called = true
reject(reason)
}
)
} else {
resolve(x)
}
} catch (error) {
if (called) return
called = true
reject(error)
}
return
}
resolve(x)
}
这段代码非常值得认真理解,因为它已经从:
javascript
"手写一个 Promise"
进入:
javascript
"理解 Promise 互操作机制"
这也是 Promise/A+ 最核心的知识。
八、async/await:Promise 的另一种使用方式
Promise 解决了异步流程组合问题,但大量:
scss
.then()
.then()
.then()
仍然可能影响代码阅读。
所以出现了:
csharp
async
await
8.1 Promise 写法
例如:
javascript
function request(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num * 2)
}, 1000)
})
}
如果第二次请求依赖第一次结果:
ini
request(5)
.then(res1 => {
return request(res1)
})
.then(res2 => {
console.log(res2)
})
使用 async/await:
scss
async function fn() {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2)
}
fn()
代码看起来更接近同步代码。
但要准确理解:
await并没有阻塞整个 JavaScript 线程。
它只是暂停当前 async function 的继续执行,把控制权交还给外部代码,等等待的 Promise settle 后再恢复。
8.2 await 为什么看起来像同步?
例如:
javascript
async function fn() {
console.log('A')
const value = await request()
console.log('B')
}
执行过程可以理解为:
rust
进入 fn
↓
输出 A
↓
遇到 await
↓
暂停 fn
↓
外部代码继续执行
↓
Promise 完成
↓
恢复 fn
↓
输出 B
所以:
javascript
await = 暂停当前 async function
而不是:
ini
await = 阻塞 JavaScript
8.3 await 后面不是 Promise 怎么办?
使用:
javascript
function request(num) {
setTimeout(() => {
console.log(num * 2)
}, 1000)
}
然后:
scss
async function fn() {
await request(1)
await request(2)
}
这里 request() 返回:
javascript
undefined
因此:
javascript
await undefined
本身并不会等待 setTimeout。
所以:
javascript
await 并不能等待一个没有返回 Promise 的异步操作。
但这里要注意一个容易产生误解的地方:
await即使后面是普通值,也仍然会让 async function 的后续部分异步恢复。
例如:
javascript
async function fn() {
console.log(1)
await 100
console.log(2)
}
fn()
console.log(3)
输出:
1
3
2
因此正确理解应该是:
javascript
await 普通值
↓
不会等待外部异步操作
↓
但当前 async function 仍会在后续 job 中恢复
8.4 async 函数返回 Promise
javascript
async function fn() {}
console.log(fn())
得到:
javascript
Promise
如果:
csharp
async function fn() {
return 10
}
那么:
php
fn().then(res => {
console.log(res)
})
输出:
10
因为 async function 的返回结果会通过 Promise 暴露出来。
可以粗略理解:
csharp
async function fn() {
return 10
}
类似于:
javascript
function fn() {
return Promise.resolve(10)
}
但严格来说两者并不总是返回同一个 Promise 引用,因此"async 就是 Promise.resolve"只是帮助理解的近似说法。
九、async/await 与 Generator:相似,但不要混为一谈
现代 JavaScript,需要更准确理解:
async/await 与 Generator + Promise 在控制流思想上高度相似,也可以使用 Generator + Promise 模拟出类似效果;但现代 JavaScript 中 async/await 是独立的语言特性,并不能简单等同于"Generator 的语法糖"。
MDN 也将两者描述为行为上相似,但不是简单的同一个机制。
所以学习 Generator 仍然非常有价值,因为它能够帮助我们理解:
暂停
恢复
值传递
异常传递
异步控制流
十、Generator:理解暂停与恢复
Generator 与普通函数最大的区别:
javascript
function* gen() {}
多了:
markdown
*
并且可以使用:
arduino
yield
10.1 yield 与 next
javascript
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
注意:
scss
gen()
不会立即把函数执行完。
它返回一个 Generator 对象。
第一次:
lua
g.next()
得到:
yaml
{
value: 1,
done: false
}
第二次:
lua
g.next()
得到:
yaml
{
value: 2,
done: false
}
第三次:
lua
g.next()
得到:
yaml
{
value: 3,
done: false
}
第四次:
lua
g.next()
得到:
yaml
{
value: undefined,
done: true
}
10.2 Generator return
如果:
javascript
function* gen() {
yield 1
yield 2
yield 3
return 4
}
那么:
lua
g.next()
g.next()
g.next()
g.next()
最后:
yaml
{
value: 4,
done: true
}
所以:
javascript
yield → 通常产生一个暂停点
return → Generator 最终结束时的 value
10.3 yield 后面接函数
例如:
javascript
function fn(num) {
console.log(num)
return num
}
function* gen() {
yield fn(1)
yield fn(2)
return 3
}
执行:
scss
const g = gen()
console.log(g.next())
此时才执行:
php
fn(1)
所以:
scss
调用 gen()
↓
不会执行函数体
↓
调用 next()
↓
执行到 yield
↓
fn(1)
↓
暂停
10.4 yield 后面接 Promise
javascript
function fn(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num)
}, 1000)
})
}
function* gen() {
yield fn(1)
yield fn(2)
return 3
}
执行:
scss
const g = gen()
console.log(g.next())
得到:
yaml
{
value: Promise,
done: false
}
注意:
Generator 本身不会自动等待这个 Promise。
所以还需要自己处理:
ini
const next1 = g.next()
next1.value.then(res1 => {
console.log(res1)
const next2 = g.next()
next2.value.then(res2 => {
console.log(res2)
console.log(g.next())
})
})
这也是后面"手动实现 async/await"的基础。
十一、Generator 的 next(value):数据如何反向传入
这是 Generator 最容易搞错的知识点之一。
例如:
javascript
function* gen() {
const num1 = yield 1
console.log(num1)
const num2 = yield 2
console.log(num2)
return 3
}
第一次:
scss
const g = gen()
g.next()
结果:
yaml
{
value: 1,
done: false
}
然后:
lua
g.next(111)
输出:
111
为什么?
因为:
arduino
const num1 = yield 1
可以理解成:
lua
第一次 next()
↓
yield 1
↓
暂停
第二次 next(111)
↓
111 替换 yield 1 这个表达式
↓
num1 = 111
所以必须记住:
next(value)传入的值,不是给"下一次 yield"使用的,而是作为"上一次 yield 表达式"的返回值。
11.1 第一次 next 传参为什么没用?
lua
g.next(111)
第一次传入:
arduino
没有对应的上一个 yield
所以通常没有意义。
只有:
scss
第二次 next(value)
开始,value 才会传递给上一次暂停的位置。
十二、Generator + Promise:手动实现异步流程
现在把两个知识点组合起来:
javascript
Generator
+
Promise
例如:
javascript
function fn(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num * 2)
}, 1000)
})
}
function* gen() {
const num1 = yield fn(1)
const num2 = yield fn(num1)
const num3 = yield fn(num2)
return num3
}
执行:
ini
const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
const next2 = g.next(res1)
next2.value.then(res2 => {
const next3 = g.next(res2)
next3.value.then(res3 => {
console.log(g.next(res3))
})
})
})
流程:
lua
g.next()
↓
得到 Promise1
↓
Promise1 完成
↓
g.next(res1)
↓
得到 Promise2
↓
Promise2 完成
↓
g.next(res2)
↓
得到 Promise3
↓
Promise3 完成
↓
g.next(res3)
↓
Generator 完成
这已经非常接近:
csharp
async function asyncFn() {
const num1 = await fn(1)
const num2 = await fn(num1)
const num3 = await fn(num2)
return num3
}
十三、使用 Generator 模拟 async/await
现在进入整个文章的综合部分。
目标:
java
function* gen() {
const num1 = yield fn(1)
const num2 = yield fn(num1)
const num3 = yield fn(num2)
return num3
}
最终能够像:
csharp
async function asyncFn() {
const num1 = await fn(1)
const num2 = await fn(num1)
const num3 = await fn(num2)
return num3
}
一样:
scss
asyncFn().then(...)
13.1 第一步:让 Generator 转换后的函数返回 Promise
javascript
function generatorToAsync(generatorFn) {
return function () {
return new Promise((resolve, reject) => {
const gen = generatorFn()
// 后续处理
})
}
}
这样:
scss
const asyncFn = generatorToAsync(gen)
console.log(asyncFn())
得到:
javascript
Promise
13.2 第二步:手动执行每一次 yield
最开始可以写死:
javascript
function generatorToAsync(generatorFn) {
return function () {
return new Promise((resolve, reject) => {
const g = generatorFn()
const next1 = g.next()
next1.value.then(res1 => {
const next2 = g.next(res1)
next2.value.then(res2 => {
const next3 = g.next(res2)
next3.value.then(res3 => {
resolve(g.next(res3).value)
})
})
})
})
}
}
它已经能够处理:
javascript
yield Promise
↓
等待 Promise
↓
next(result)
↓
继续执行
但问题非常明显:
arduino
2 个 yield?
3 个 yield?
10 个 yield?
100 个 yield?
不能全部写死。
十四、通用 Generator Runner:从固定次数到任意 await
真正的关键是:
javascript
function go(key, arg)
核心实现:
javascript
function generatorToAsync(generatorFn) {
return function () {
const gen = generatorFn.apply(this, arguments)
return new Promise((resolve, reject) => {
function go(key, arg) {
let result
try {
result = gen[key](arg)
} catch (error) {
reject(error)
return
}
const { value, done } = result
if (done) {
resolve(value)
return
}
Promise.resolve(value).then(
value => go('next', value),
error => go('throw', error)
)
}
go('next')
})
}
}
这段代码需要重点理解。
14.1 go('next')
第一次:
go
go('next')
相当于:
lua
gen.next()
Generator 开始执行:
php
yield fn(1)
返回:
yaml
{
value: Promise,
done: false
}
14.2 Promise.resolve(value)
这里故意使用:
javascript
Promise.resolve(value)
而不是假设:
scss
value.then(...)
因为:
arduino
yield 1
也是合法的。
所以:
javascript
Promise.resolve(1)
可以统一处理:
javascript
普通值
Promise
thenable
14.3 Promise 成功后调用 next
go
Promise.resolve(value).then(
value => go('next', value),
error => go('throw', error)
)
如果:
javascript
Promise fulfilled
就:
lua
gen.next(value)
相当于:
php
const num1 = yield fn(1)
中的:
ini
num1 = result
14.4 Promise 失败后调用 throw
如果:
php
fn(1)
reject:
go
go('throw', error)
相当于:
vbnet
gen.throw(error)
这样 Generator 内部就可以:
vbnet
try {
const value = yield request()
} catch (error) {
console.error(error)
}
这也是 Generator 异常传播能够模拟 async/await 错误处理的重要原因。
14.5 Generator 执行结束
如果:
ini
done === true
说明:
javascript
Generator 执行结束
那么:
scss
resolve(value)
即可。
最终:
scss
const asyncFn = generatorToAsync(gen)
asyncFn().then(res => {
console.log(res)
})
就可以得到:
8
十五、async/await、Promise、Generator 到底是什么关系?
现在把整个文章串起来。
javascript
Promise
│
├── 描述异步操作的最终结果
│
├── then/catch/finally
│
├── 链式调用
│
└── 并发组合
│
↓
async/await
│
├── 让 Promise 链更容易阅读
├── await 暂停当前 async function
└── async function 返回 Promise
Generator
│
├── yield 暂停
├── next 恢复
├── next(value) 传入结果
└── throw(error) 传入异常
│
↓
Generator + Promise
│
↓
异步流程控制器
│
↓
可以模拟 async/await 的控制流
真正应该记住的是:
Promise 是异步结果的抽象;
async/await 是消费 Promise 的现代语法;
Generator 提供了可暂停、可恢复的执行模型;
Generator + Promise 可以构造出类似 async/await 的异步控制流。
十六、现代 JavaScript 异步编程:从"会用"进入工程实践
在很久之前我们学习了上述内容后,就已经是完整的学习了异步编程的所有核心内容,
但如果目标是 2026 年的前端工程开发,仅仅会:
javascript
Promise
async/await
Generator
还不够。
实际项目中的异步问题更多集中在:
并发
取消
超时
重试
错误传播
请求竞态
重复请求
资源释放
16.1 Promise 并发与串行
下面两个写法完全不同。
串行
csharp
const user = await getUser()
const orders = await getOrders(user.id)
const coupons = await getCoupons(user.id)
执行:
getUser
↓
getOrders
↓
getCoupons
总耗时大致:
T1 + T2 + T3
并行
如果三个请求互不依赖:
scss
const [user, orders, coupons] = await Promise.all([
getUser(),
getOrders(),
getCoupons()
])
执行:
javascript
getUser ──────┐
getOrders ──────┼──→ Promise.all
getCoupons ─────┘
总耗时更接近:
scss
max(T1, T2, T3)
所以:
await不是越多越好,需要判断任务之间有没有依赖关系。
16.2 不要制造无意义的串行等待
例如:
csharp
const a = await requestA()
const b = await requestB()
如果:
css
B 不依赖 A
更合理的是:
scss
const [a, b] = await Promise.all([
requestA(),
requestB()
])
这也是实际项目中 Promise 并发能力的重要应用。
16.3 请求取消:AbortController
Promise 本身没有通用的"一键取消"机制。
也就是说:
arduino
const promise = fetch(...)
不能简单:
arduino
promise.cancel()
现代 Web API 通常通过:
AbortController
取消底层异步操作。
例如:
scss
const controller = new AbortController()
fetch('/api/user', {
signal: controller.signal
})
controller.abort()
这在:
搜索联想
页面卸载
组件销毁
请求超时
竞态请求
中非常常见。
16.4 超时控制
可以组合:
javascript
Promise.race()
实现教学版超时:
javascript
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'))
}, ms)
})
}
Promise.race([
fetch('/api/data'),
timeout(5000)
])
但需要注意:
Promise.race()只是让返回的 Promise 更早结束,并不自动取消fetch。
真实工程中通常应该把:
bash
timeout
+
AbortController
组合起来。
16.5 错误传播
Promise 链中的异常会沿着链传播。
例如:
javascript
request()
.then(() => {
throw new Error('failed')
})
.then(() => {
console.log('不会执行')
})
.catch(error => {
console.error(error)
})
所以:
arduino
then 中 throw
↓
当前链进入 rejected
↓
寻找后续 rejection handler
↓
catch
对应 async/await:
vbnet
try {
await request()
throw new Error('failed')
} catch (error) {
console.error(error)
}
这也是 async/await 在复杂业务中可读性较高的重要原因。
十七、2026 年前端异步代码需要特别注意的几个问题
17.1 AI 生成异步代码时,不能只看"能不能运行"
现在使用 AI 生成:
javascript
Promise.all(...)
async/await
retry
Scheduler
fetch
非常容易。
但真正容易出问题的是:
javascript
有没有并发过量?
有没有请求泄漏?
有没有忘记 return Promise?
错误有没有被吞掉?
失败后 count 有没有恢复?
组件卸载后请求还在不在?
请求是否存在竞态?
是否真的取消了底层任务?
例如:
python
items.map(async item => {
await request(item)
})
很多人以为:
python
map + async
就是"等待所有任务"。
实际上 map() 返回的是:
css
Promise[]
应该:
javascript
await Promise.all(
items.map(item => request(item))
)
AI 可以生成代码,但这些异步控制流的正确性仍然需要工程师自己判断。
17.2 Promise floating
例如:
scss
doSomething()
.then(() => {
doAnotherThing()
})
如果:
scss
doAnotherThing()
本身返回 Promise,却没有:
kotlin
return
那么外部链无法知道它什么时候结束。
更正确:
scss
doSomething()
.then(() => {
return doAnotherThing()
})
或者:
scss
doSomething()
.then(() => doAnotherThing())
否则就会产生所谓的:
arduino
floating promise
这在请求链、保存操作、AI 流式任务中都很容易出现。
十八、高频面试题
这些问题不要因为已经进入 async/await 时代就跳过。
Promise 基础
1. Promise 有几种状态?
pending
fulfilled
rejected
并且:
pending → fulfilled
pending → rejected
不能反向。
2. 为什么 Promise 状态只能改变一次?
因为 Promise 表示一个异步操作的最终结果。
一旦 settled:
fulfilled
后续:
scss
reject(...)
不能重新改变结果。
3. executor 是同步执行还是异步执行?
Promise constructor 中的 executor:
javascript
new Promise(executor)
是同步执行的。
例如:
javascript
console.log(1)
new Promise(resolve => {
console.log(2)
resolve()
})
console.log(3)
输出:
1
2
3
但:
scss
.then(...)
回调是异步调度的。
4. 为什么:
javascript
Promise.resolve(1).then(() => {
console.log(2)
})
console.log(3)
输出:
3
2
因为 Promise reaction 不会在当前同步代码执行过程中直接调用,而是异步调度。
Promise 链
5. then 为什么返回 Promise?
因为需要支持:
scss
p
.then(...)
.then(...)
.then(...)
每次 then() 都创建一个新的 Promise。
6. then 返回普通值会怎么样?
javascript
.then(() => 100)
下一个 Promise:
scss
fulfilled(100)
7. then 返回 Promise 会怎么样?
javascript
.then(() => Promise.resolve(100))
下一个 Promise 会跟随返回 Promise 的最终状态。
8. then 中抛异常怎么办?
javascript
Promise.resolve()
.then(() => {
throw new Error('error')
})
后续 Promise:
rejected
可以通过:
csharp
.catch(...)
处理。
9. catch 是什么?
本质上:
arduino
promise.catch(onRejected)
相当于:
arduino
promise.then(undefined, onRejected)
10. finally 有什么作用?
用于无论成功还是失败都需要执行的清理逻辑:
arduino
promise.finally(cleanup)
例如:
关闭 loading
释放资源
清理状态
Promise 静态方法
11. Promise.all、race、allSettled、any 有什么区别?
| API | 成功条件 | 失败条件 |
|---|---|---|
all |
全部成功 | 任意失败 |
race |
第一个 settled | 第一个 settled 如果失败 |
allSettled |
全部 settled | 本身不会因为输入失败而 rejected |
any |
任意成功 | 全部失败 |
记忆:
python
all → 全部成功
race → 谁先结束听谁的
allSettled→ 全部结束
any → 一个成功就行
12. Promise.all 会取消其他请求吗?
不会。
scss
Promise.all([
requestA(),
requestB(),
requestC()
])
如果 A 失败:
javascript
Promise.all → rejected
但 B、C 本身仍可能继续运行。
如果需要取消,需要配合底层 API 的取消机制,例如 AbortController。
13. Promise.any 全部失败会返回什么?
AggregateError
而不是普通的单一 Error。
十九、async/await 高频面试题
14. async 函数返回什么?
永远返回 Promise。
csharp
async function fn() {
return 1
}
相当于对外提供一个 Promise 结果。
15. await 会阻塞 JavaScript 主线程吗?
不会。
它只是暂停当前 async function 的后续执行。
16. await 后面一定要 Promise 吗?
不一定。
csharp
await 100
是合法的。
但如果想等待某个真正的异步操作,该操作必须能够提供 Promise 或 thenable 等可等待结果。
17. 为什么两个 await 会串行?
csharp
const a = await requestA()
const b = await requestB()
因为:
javascript
requestA 完成
↓
恢复 async function
↓
执行 requestB
如果两个任务没有依赖关系,可以:
scss
const [a, b] = await Promise.all([
requestA(),
requestB()
])
18. async/await 是不是 Generator 的语法糖?
更准确的回答:
两者控制流思想相似,历史上 Generator + Promise 常用于实现类似 async/await 的异步流程;但现代 JavaScript 中 async/await 是独立的语言特性,不能简单等同为 Generator 的语法糖。
二十、Generator 高频面试题
19. Generator 和普通函数有什么区别?
javascript
function* gen() {}
Generator:
lua
不会一次执行到底
可以 yield 暂停
可以 next() 恢复
20. next() 返回什么?
bash
{
value,
done
}
其中:
javascript
value → 当前 yield / return 的值
done → Generator 是否执行结束
21. next(value) 的 value 传给谁?
传给:
arduino
const value = yield ...
中的:
arduino
yield 表达式
也就是:
arduino
上一次 yield
而不是下一次 yield。
22. 为什么第一次 next(value) 通常没有意义?
因为第一次执行时:
还没有暂停过
所以没有一个上一次的 yield 可以接收这个 value。
23. Generator 能不能直接等待 Promise?
不能自动等待。
:
javascript
yield Promise
只是把 Promise 作为:
scss
next().value
返回出来。
必须自己写 runner,或者使用其他机制处理。
二十一、手写 Promise 高频面试题
如果面试要求:
手写一个 Promise
通常不是要求你几分钟内完整复刻 ECMAScript Promise。
更重要的是你是否理解这些核心:
markdown
1. pending / fulfilled / rejected
2. 状态只能改变一次
3. executor 同步执行
4. executor 异常转 rejected
5. then 回调保存
6. then 返回新的 Promise
7. 链式调用
8. 普通值处理
9. Promise / thenable 处理
10. 异常传播
11. 自引用检测
12. 异步调度
如果能进一步实现:
javascript
Promise.resolve
Promise.reject
Promise.all
Promise.race
Promise.allSettled
Promise.any
以及:
thenable assimilation
说明已经真正理解 Promise 的核心机制。
二十二、最终总结:真正需要掌握的不是 API,而是异步控制流
我们整篇文章可以最终压缩成下面这张知识图:
typescript
JavaScript 异步编程
│
┌─────────────┴─────────────┐
↓ ↓
Promise Generator
│ │
┌──────┼──────┐ ┌──────┼──────┐
↓ ↓ ↓ ↓ ↓ ↓
状态 then 静态方法 yield next throw
│ │ │ │ │ │
│ │ ├─ all │ │ │
│ │ ├─ race │ │ │
│ │ ├─ allSettled │ │ │
│ │ └─ any │ │ │
│ │
│ ├─ 链式调用
│ ├─ 错误传播
│ ├─ thenable
│ └─ Resolution Procedure
│
└──────────────┐
↓
async / await
│
┌──────┼──────┐
↓ ↓ ↓
串行 并发 错误处理
│
↓
Promise.all
Scheduler
AbortController
Timeout
Retry
│
↓
工程级异步系统
最终真正应该形成的能力不是:
"我会写 Promise。"
而是:
面对任何一个异步问题,我能够判断它属于串行、并行、竞争、限流、取消、超时、重试还是错误传播问题,并选择合适的异步控制方式。
这也是从"会使用 async/await"到"真正理解 JavaScript 异步模型"的区别。