3. JavaScript 异步编程:Promise、async/await、Generator 与异步调度

从 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' }

因此可以总结:

  1. 执行 resolve(),Promise 可以进入 fulfilled;
  2. 执行 reject(),Promise 可以进入 rejected;
  3. Promise 状态只能从 pending 进入 settled 状态;
  4. 一旦 settled,后续的 resolve() / reject() 不再改变它;
  5. 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+ 官方规范

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 异步模型"的区别。

相关推荐
国奉1 小时前
iOS 音频格式转换怎么实现?从 AVAudioFile、AAC、MP3 到 FLAC 与批量转码架构
前端·后端
易朵朵1 小时前
package.json 中的 `vue-router` 详解
前端·vue.js
IT_陈寒1 小时前
Vite的静态资源引用把我坑惨了
前端·人工智能·后端
风骏时光牛马1 小时前
AI工作流全链路自动化落地实践
前端
kyriewen1 小时前
5 次优化让首屏快 3.6 秒,只有 1 次是改代码
前端·javascript·程序员
编程老船长1 小时前
模型中立——把大模型做成"可替换零件",而不是焊死在业务里
java·前端·后端
禁止摆烂_才浅1 小时前
Axios 完整封装合集(鉴权 + 重复拦截 + Loading + 缓存 + 统一错误 + 请求重试|全代码逐行注释)
前端·javascript·axios
陈随易1 小时前
傻瓜式UX:ERP的致命糖衣
前端·后端·程序员
不可能片场1 小时前
Windows 中文传参坑 命令行乱码变量来救场
前端·electron