Skip to content

Promise 原理与实现 ​

Promise 基础概念 ​

什么是 Promise ​

Promise 是 JavaScript 中处理异步操作的核心机制。在 Promise 出现之前,异步编程主要依赖回调函数,这在嵌套复杂时会形成"回调地狱"。Promise 提供了一种更优雅的方式来组织和管理异步代码——它代表一个未来某个时刻才会确定的值。

从本质上看,Promise 是一个状态机。它是一个对象,内部维护着异步操作的状态,并在状态确定后通知所有注册的观察者。

三种状态与不可逆转 ​

Promise 有且仅有三种状态:

┌──────────┐     resolve(value)     ┌───────────┐
│          │ ─────────────────────→ │           │
│ Pending  │                        │ Fulfilled │
│ (等待态)  │                        │  (成功态)  │
│          │                        │           │
└──────────┘                        └───────────┘
      │
      │     reject(reason)          ┌───────────┐
      └───────────────────────────→ │           │
                                    │ Rejected  │
                                    │  (失败态)  │
                                    │           │
                                    └───────────┘
  • Pending(等待态):初始状态,既不是成功也不是失败
  • Fulfilled(成功态):操作成功完成,Promise 持有一个确定的值(value)
  • Rejected(失败态):操作失败,Promise 持有一个确定的原因(reason)

状态不可逆转是 Promise 最重要的设计原则之一。一旦状态从 Pending 变为 Fulfilled 或 Rejected,就永远不会再改变。这意味着:

js
const p = new Promise((resolve, reject) => {
  resolve('success')
  reject('error')
  resolve('another success')
})

p.then(value => console.log(value))

输出结果只有 success。第二次调用 reject 和第三次调用 resolve 都被忽略了,因为状态已经从 Pending 变为 Fulfilled,不可逆转。

这一特性保证了 Promise 的确定性——无论何时访问一个已经 settled 的 Promise,得到的结果都是一样的。

状态变更的微任务特性 ​

Promise 的状态变更本身是同步的,但 then 注册的回调是以**微任务(Microtask)**的方式异步执行的:

js
console.log('1')

const p = new Promise((resolve) => {
  console.log('2')
  resolve('3')
  console.log('4')
})

p.then(value => console.log(value))

console.log('5')

输出顺序:1 → 2 → 4 → 5 → 3

executor 函数(传入 new Promise 的函数)是同步执行的,所以 2 和 4 会在同步流程中按顺序输出。而 then 的回调被放入微任务队列,等到当前同步代码全部执行完毕后才会运行,所以 3 最后输出。


then/catch/finally 链式调用原理 ​

then 的核心机制 ​

then 方法是 Promise 最核心的方法。它接收两个可选参数:

js
promise.then(onFulfilled, onRejected)

then 的关键特性:每次调用都返回一个新的 Promise。这是链式调用的基础。

js
const p1 = Promise.resolve(1)
const p2 = p1.then(v => v + 1)
const p3 = p2.then(v => v + 1)

console.log(p1 === p2)
console.log(p2 === p3)

两个输出都是 false,说明 then 每次都创建了新的 Promise 实例。

新 Promise 的状态由 then 的回调函数返回值决定:

js
Promise.resolve(1)
  .then(v => v * 2)
  .then(v => {
    throw new Error('fail')
  })
  .then(
    v => console.log('fulfilled:', v),
    r => console.log('rejected:', r.message)
  )

输出:rejected: fail。第二个 then 的回调抛出异常,导致它返回的 Promise 变为 Rejected,于是第三个 then 的 onRejected 被触发。

值穿透 ​

当 then 接收的不是函数时,会发生值穿透——值或状态会直接传递到下一个有效的 then:

js
Promise.resolve('hello')
  .then(null)
  .then(undefined)
  .then(123)
  .then(v => console.log(v))

输出 hello。非函数参数被忽略,值一路穿透到最终的 then。

值穿透的内部机制等价于:

js
Promise.resolve('hello')
  .then(v => v)
  .then(v => v)
  .then(v => v)
  .then(v => console.log(v))

当 onFulfilled 不是函数时,它会被替换为 v => v(恒等函数);当 onRejected 不是函数时,它会被替换为 r => { throw r }(抛出函数)。这保证了值和错误都能正确穿透。

错误冒泡 ​

错误会沿着链一直向下传播,直到遇到一个错误处理函数:

js
Promise.reject('error')
  .then(v => console.log('then1:', v))
  .then(v => console.log('then2:', v))
  .then(v => console.log('then3:', v))
  .catch(r => console.log('caught:', r))

输出 caught: error。三个 then 都没有提供 onRejected 回调,错误不断冒泡,最终被 catch 捕获。

这与 try/catch 的行为类似——错误会跳过所有不处理它的节点,直到被捕获。

catch 的本质 ​

catch 本质上就是 then(null, onRejected) 的语法糖:

js
Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

但 catch 与 then 的第二个参数有一个重要区别:catch 能捕获前一个 then 的 onFulfilled 回调中抛出的异常,而 then 的第二个参数只能捕获前一个 Promise 的错误。

js
Promise.resolve('ok').then(
  v => { throw new Error('then error') },
  r => console.log('onRejected:', r)
)

Promise.resolve('ok')
  .then(v => { throw new Error('then error') })
  .catch(r => console.log('catch:', r.message))

第一个写法中 onRejected 不会被触发,因为异常发生在同一个 then 的 onFulfilled 中。第二个写法中 catch 能正确捕获异常,因为 catch 作用于 then 返回的新 Promise。

finally 的实现原理 ​

finally 不管 Promise 最终状态如何都会执行,且它的回调不接收任何参数,也不改变 Promise 的值(除非抛出异常或返回 rejected Promise):

js
Promise.resolve(42)
  .finally(() => console.log('cleanup'))
  .then(v => console.log(v))

输出:cleanup 然后 42。finally 透传了原始值。

js
Promise.resolve(42)
  .finally(() => { throw new Error('oops') })
  .then(v => console.log(v))
  .catch(r => console.log('error:', r.message))

输出 error: oops。finally 回调中抛出异常时会覆盖原始状态。

finally 的实现等价于:

js
Promise.prototype.finally = function(callback) {
  return this.then(
    value => Promise.resolve(callback()).then(() => value),
    reason => Promise.resolve(callback()).then(() => { throw reason })
  )
}

Promise A+ 规范核心要点 ​

Promise A+ 规范是 Promise 的行业标准,ES6 的 Promise 在此基础上扩展了更多 API。以下是规范的核心要点:

1. 术语 ​

  • Promise:一个拥有 then 方法的对象或函数,其行为符合本规范
  • thenable:一个定义了 then 方法的对象或函数
  • value:任何合法的 JavaScript 值(包括 undefined、thenable 或 Promise)
  • exception:使用 throw 语句抛出的值
  • reason:表示 Promise 被拒绝原因的值

2. Promise 状态 ​

  • Promise 必须处于 Pending、Fulfilled、Rejected 三种状态之一
  • Pending 状态可以转变为 Fulfilled 或 Rejected
  • Fulfilled 和 Rejected 状态不可再变更
  • Fulfilled 状态必须有一个不可变的 value
  • Rejected 状态必须有一个不可变的 reason

3. then 方法 ​

Promise 必须提供 then 方法来访问当前或最终的值/原因:

promise.then(onFulfilled, onRejected)

核心规则:

  • onFulfilled 和 onRejected 都是可选的,如果不是函数则忽略
  • onFulfilled 必须在 Promise fulfilled 后调用,以 Promise 的值为第一个参数,且只能调用一次
  • onRejected 必须在 Promise rejected 后调用,以 Promise 的原因为第一个参数,且只能调用一次
  • onFulfilled 和 onRejected 只有在执行上下文栈仅包含平台代码时才可被调用(即异步执行)
  • then 可以在同一个 Promise 上被多次调用,回调按注册顺序依次执行
  • then 必须返回一个新的 Promise

4. Promise 解决过程(Resolution Procedure) ​

这是规范中最复杂也最关键的部分,定义为 [[Resolve]](promise, x),其中 x 是 onFulfilled 或 onRejected 的返回值:

┌─────────────────────────────────────────────────────────┐
│              [[Resolve]](promise2, x)                   │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. x === promise2                                      │
│     → reject(TypeError) 防止循环引用                      │
│                                                         │
│  2. x 是 Promise                                        │
│     → 采用 x 的状态                                      │
│                                                         │
│  3. x 是对象或函数                                       │
│     → 尝试取 x.then                                     │
│     → 如果 then 是函数,以 x 为 this 调用                 │
│     → 如果 then 不是函数,fulfill(x)                     │
│                                                         │
│  4. x 不是对象也不是函数                                  │
│     → fulfill(x)                                        │
│                                                         │
└─────────────────────────────────────────────────────────┘

这个解决过程保证了不同 Promise 实现之间的互操作性——只要实现了 then 方法的对象(thenable)都能被正确处理。


手写完整 Promise 实现 ​

基础结构 ​

js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (value instanceof MyPromise) {
        value.then(resolve, reject)
        return
      }
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }

    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
}

关键设计点:

  • onFulfilledCallbacks 和 onRejectedCallbacks 是数组,因为同一个 Promise 可以多次调用 then
  • resolve 内部检查 value instanceof MyPromise,如果 resolve 的值本身是 Promise,则等待其 settled
  • executor 用 try/catch 包裹,确保同步异常能被捕获并 reject

resolvePromise —— 核心解决过程 ​

这是 Promise A+ 规范中最复杂的部分,也是手写 Promise 的关键:

js
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }

  if (x instanceof MyPromise) {
    x.then(
      y => resolvePromise(promise2, y, resolve, reject),
      reject
    )
    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(promise2, y, resolve, reject)
          },
          r => {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

详解:

  1. 循环引用检测:如果 then 返回的 Promise 和回调返回值相同,形成循环引用,直接抛 TypeError
  2. x 是 MyPromise 实例:递归解析,因为 then 回调返回的 Promise 可能 resolve 另一个 Promise
  3. x 是 thenable 对象:用 called 标志位防止多次调用。这是为了兼容不规范的 thenable 实现——有些库可能同时调用 resolve 和 reject,或者多次调用
  4. x 是普通值:直接 resolve

完整的 then 方法 ​

js
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

  const promise2 = new MyPromise((resolve, reject) => {
    const fulfilledMicrotask = () => {
      queueMicrotask(() => {
        try {
          const x = onFulfilled(this.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    }

    const rejectedMicrotask = () => {
      queueMicrotask(() => {
        try {
          const x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    }

    if (this.status === FULFILLED) {
      fulfilledMicrotask()
    } else if (this.status === REJECTED) {
      rejectedMicrotask()
    } else if (this.status === PENDING) {
      this.onFulfilledCallbacks.push(fulfilledMicrotask)
      this.onRejectedCallbacks.push(rejectedMicrotask)
    }
  })

  return promise2
}

核心设计要点:

  • 值穿透:当 onFulfilled 不是函数时,默认为 value => value;onRejected 不是函数时,默认为 reason => { throw reason }
  • 异步执行:使用 queueMicrotask 确保回调异步执行,符合规范要求
  • 返回新 Promise:then 总是返回 promise2,链式调用的基础
  • 三种状态处理:Fulfilled/Rejected 立即注册微任务;Pending 时将微任务推入回调数组,等待状态变更后执行

catch 和 finally ​

js
MyPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

MyPromise.prototype.finally = function(callback) {
  return this.then(
    value => MyPromise.resolve(callback()).then(() => value),
    reason => MyPromise.resolve(callback()).then(() => { throw reason })
  )
}

完整实现汇总 ​

将上面所有部分组合起来就是一个完整的、符合 Promise A+ 规范的实现:

js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }

  if (x instanceof MyPromise) {
    x.then(
      y => resolvePromise(promise2, y, resolve, reject),
      reject
    )
    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(promise2, y, resolve, reject)
          },
          r => {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (value instanceof MyPromise) {
        value.then(resolve, reject)
        return
      }
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }

    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

    const promise2 = new MyPromise((resolve, reject) => {
      const fulfilledMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }

      const rejectedMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }

      if (this.status === FULFILLED) {
        fulfilledMicrotask()
      } else if (this.status === REJECTED) {
        rejectedMicrotask()
      } else if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(fulfilledMicrotask)
        this.onRejectedCallbacks.push(rejectedMicrotask)
      }
    })

    return promise2
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason })
    )
  }
}

Promise 静态方法手写实现 ​

Promise.resolve ​

Promise.resolve 将一个值转换为 Promise。需要处理三种情况:

js
MyPromise.resolve = function(value) {
  if (value instanceof MyPromise) {
    return value
  }
  return new MyPromise((resolve) => {
    resolve(value)
  })
}

如果传入的已经是 MyPromise 实例,直接返回(幂等性)。否则创建一个新的已 fulfilled 的 Promise。注意 resolve 内部已经处理了 thenable 的情况。

Promise.reject ​

Promise.reject 始终返回一个 rejected 的 Promise,不做任何解包:

js
MyPromise.reject = function(reason) {
  return new MyPromise((_, reject) => {
    reject(reason)
  })
}

与 resolve 不同,即使 reason 是一个 Promise,也不会解包,而是直接作为 reject 的原因。

Promise.all ​

等待所有 Promise 都 fulfilled,或者任意一个 rejected:

js
MyPromise.all = function(promises) {
  return new MyPromise((resolve, reject) => {
    const result = []
    let count = 0
    const promiseArr = Array.from(promises)

    if (promiseArr.length === 0) {
      return resolve(result)
    }

    promiseArr.forEach((p, index) => {
      MyPromise.resolve(p).then(
        value => {
          result[index] = value
          count++
          if (count === promiseArr.length) {
            resolve(result)
          }
        },
        reason => {
          reject(reason)
        }
      )
    })
  })
}

关键点:

  • 使用 result[index] 而非 push,保证结果顺序与输入顺序一致
  • 使用计数器 count 而非检查 result.length,因为数组的稀疏特性可能导致 length 不准确
  • 任意一个 reject 就立即 reject 整个 Promise
  • 空数组直接 resolve
  • 使用 MyPromise.resolve(p) 包装,兼容非 Promise 值

Promise.allSettled ​

等待所有 Promise settled(无论成功或失败),返回每个结果的状态和值:

js
MyPromise.allSettled = function(promises) {
  return new MyPromise((resolve) => {
    const result = []
    let count = 0
    const promiseArr = Array.from(promises)

    if (promiseArr.length === 0) {
      return resolve(result)
    }

    promiseArr.forEach((p, index) => {
      MyPromise.resolve(p).then(
        value => {
          result[index] = { status: 'fulfilled', value }
          count++
          if (count === promiseArr.length) {
            resolve(result)
          }
        },
        reason => {
          result[index] = { status: 'rejected', reason }
          count++
          if (count === promiseArr.length) {
            resolve(result)
          }
        }
      )
    })
  })
}

allSettled 永远不会 reject。每个结果是 { status, value } 或 { status, reason } 的格式。

Promise.race ​

返回第一个 settled 的 Promise 的结果(无论成功或失败):

js
MyPromise.race = function(promises) {
  return new MyPromise((resolve, reject) => {
    const promiseArr = Array.from(promises)

    promiseArr.forEach(p => {
      MyPromise.resolve(p).then(resolve, reject)
    })
  })
}

race 的实现非常简洁。由于 Promise 状态不可逆转,第一个 settle 的 Promise 会调用 resolve 或 reject,后续的调用都会被忽略。

注意:如果传入空数组,返回的 Promise 将永远处于 Pending 状态。

Promise.any ​

返回第一个 fulfilled 的 Promise 的值。如果所有 Promise 都 rejected,则 reject 一个 AggregateError:

js
MyPromise.any = function(promises) {
  return new MyPromise((resolve, reject) => {
    const errors = []
    let count = 0
    const promiseArr = Array.from(promises)

    if (promiseArr.length === 0) {
      return reject(new AggregateError([], 'All promises were rejected'))
    }

    promiseArr.forEach((p, index) => {
      MyPromise.resolve(p).then(
        value => {
          resolve(value)
        },
        reason => {
          errors[index] = reason
          count++
          if (count === promiseArr.length) {
            reject(new AggregateError(errors, 'All promises were rejected'))
          }
        }
      )
    })
  })
}

any 可以看作是 all 的"镜像"——all 是任意一个 reject 就失败,any 是任意一个 fulfill 就成功。


Promise 并发控制 ​

在实际场景中,我们经常需要同时发起大量异步请求,但又不能让它们全部并行(可能导致服务端压力过大、浏览器连接数耗尽等问题)。Promise 并发控制就是限制同时执行的异步任务数量。

基于池模型的并发控制 ​

js
function concurrencyLimit(tasks, limit) {
  return new Promise((resolve, reject) => {
    const results = []
    let running = 0
    let index = 0
    let settled = false

    function next() {
      while (running < limit && index < tasks.length) {
        const currentIndex = index++
        running++

        Promise.resolve(tasks[currentIndex]())
          .then(value => {
            results[currentIndex] = value
          })
          .catch(err => {
            if (!settled) {
              settled = true
              reject(err)
            }
          })
          .finally(() => {
            running--
            if (!settled) {
              if (index >= tasks.length && running === 0) {
                settled = true
                resolve(results)
              } else {
                next()
              }
            }
          })
      }
    }

    next()
  })
}

使用示例:

js
const tasks = [
  () => fetch('/api/1').then(r => r.json()),
  () => fetch('/api/2').then(r => r.json()),
  () => fetch('/api/3').then(r => r.json()),
  () => fetch('/api/4').then(r => r.json()),
  () => fetch('/api/5').then(r => r.json()),
]

concurrencyLimit(tasks, 2).then(results => {
  console.log(results)
})

执行流程:

时间轴 →

Task 1: ████████
Task 2: ██████████████
Task 3:         ████████████
Task 4:                     ██████
Task 5:               ██████████████

并发数:  2       2      2     2    1

最多同时执行 2 个任务,一个完成立即启动下一个。

基于类的并发调度器 ​

更通用的实现,支持动态添加任务:

js
class Scheduler {
  constructor(maxConcurrency) {
    this.maxConcurrency = maxConcurrency
    this.running = 0
    this.queue = []
  }

  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject })
      this.run()
    })
  }

  run() {
    while (this.running < this.maxConcurrency && this.queue.length > 0) {
      const { task, resolve, reject } = this.queue.shift()
      this.running++

      Promise.resolve(task())
        .then(resolve, reject)
        .finally(() => {
          this.running--
          this.run()
        })
    }
  }
}

使用示例:

js
const scheduler = new Scheduler(2)

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))

const addTask = (time, value) => {
  scheduler.add(() => delay(time).then(() => {
    console.log(value)
    return value
  }))
}

addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')

输出顺序:2 → 3 → 1 → 4

0ms    500ms  800ms  1000ms 1200ms
|------|------|------|------|
Task1: ████████████████████        → 1000ms 输出 '1'
Task2: ██████████                  →  500ms 输出 '2'
Task3:           ██████            →  800ms 输出 '3'
Task4:           ████████████      → 1200ms 输出 '4' (非900ms,因为等Task2完成才开始)

实际上 Task3 在 500ms(Task2 完成时)才开始,在 800ms 完成;Task4 也在 500ms 开始(因为并发数恢复到 1 后立即调度),在 900ms 完成。但 Task1 在 1000ms 才完成,所以输出顺序是 2(500ms) → 3(800ms) → 4(900ms) → 1(1000ms)。

修正后的输出顺序应为:2 → 3 → 4 → 1。


async/await 的本质 ​

Generator 基础 ​

要理解 async/await,首先需要理解 Generator 函数。Generator 是一种可以暂停和恢复执行的特殊函数:

js
function* gen() {
  const a = yield 1
  const b = yield 2
  return a + b
}

const it = gen()
console.log(it.next())
console.log(it.next(10))
console.log(it.next(20))

输出:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 30, done: true }

关键特性:

  • yield 暂停函数执行,将 yield 后面的值作为 next() 的返回值
  • next(val) 恢复函数执行,并将 val 作为上一个 yield 表达式的返回值
  • Generator 函数调用不会立即执行,而是返回一个迭代器

async/await = Generator + 自动执行器 ​

async/await 本质上是 Generator 函数 + 自动执行器的语法糖。对比以下两种写法:

js
async function fetchData() {
  const res1 = await fetch('/api/1')
  const data1 = await res1.json()
  const res2 = await fetch('/api/2?id=' + data1.id)
  const data2 = await res2.json()
  return data2
}

等价的 Generator 写法:

js
function* fetchData() {
  const res1 = yield fetch('/api/1')
  const data1 = yield res1.json()
  const res2 = yield fetch('/api/2?id=' + data1.id)
  const data2 = yield res2.json()
  return data2
}

区别在于:async/await 自带执行器,Generator 需要手动驱动或借助执行器。

自动执行器的实现 ​

js
function co(generatorFn) {
  return function(...args) {
    const gen = generatorFn.apply(this, args)

    return new Promise((resolve, reject) => {
      function step(nextFn) {
        let next
        try {
          next = nextFn()
        } catch (e) {
          return reject(e)
        }

        if (next.done) {
          return resolve(next.value)
        }

        Promise.resolve(next.value).then(
          value => step(() => gen.next(value)),
          reason => step(() => gen.throw(reason))
        )
      }

      step(() => gen.next())
    })
  }
}

使用方式:

js
const fetchData = co(function* () {
  const res1 = yield fetch('/api/1')
  const data1 = yield res1.json()
  return data1
})

fetchData().then(data => console.log(data))

执行器的核心逻辑:

  1. 调用 gen.next(value) 恢复 Generator 执行,获取 yield 出的值
  2. 将 yield 出的值包装为 Promise(Promise.resolve(next.value))
  3. 等待 Promise settled:fulfilled 则将值传回 Generator 继续执行;rejected 则向 Generator 抛出错误
  4. 递归执行,直到 Generator 返回 { done: true }
  5. 最终结果作为外层 Promise 的 resolve 值

手写 async/await 的 polyfill ​

一个更完整的 polyfill,模拟 Babel 对 async/await 的转译:

js
function asyncToGenerator(generatorFn) {
  return function(...args) {
    const gen = generatorFn.apply(this, args)

    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result
        try {
          result = gen[key](arg)
        } catch (e) {
          return reject(e)
        }

        const { value, done } = result

        if (done) {
          resolve(value)
        } else {
          Promise.resolve(value).then(
            val => step('next', val),
            err => step('throw', err)
          )
        }
      }

      step('next', undefined)
    })
  }
}

Babel 转译示例——原始代码:

js
async function example() {
  const a = await Promise.resolve(1)
  const b = await Promise.resolve(2)
  return a + b
}

Babel 转译后(简化版):

js
function example() {
  return asyncToGenerator(function* () {
    const a = yield Promise.resolve(1)
    const b = yield Promise.resolve(2)
    return a + b
  }).call(this)
}

async 关键字被替换为 asyncToGenerator 包裹的 Generator 函数,await 被替换为 yield。

async 函数的返回值 ​

async 函数始终返回 Promise:

js
async function fn1() { return 42 }
async function fn2() { return Promise.resolve(42) }
async function fn3() { throw new Error('oops') }

fn1().then(v => console.log(v))
fn2().then(v => console.log(v))
fn3().catch(e => console.log(e.message))
  • fn1 返回 Promise.resolve(42)
  • fn2 返回值是 Promise,会被自动解包(类似 resolve 的行为)
  • fn3 抛出异常等价于返回 Promise.reject(new Error('oops'))

Promise 常见陷阱与反模式 ​

1. 回调地狱的变形——Promise 嵌套 ​

js
getUserInfo().then(user => {
  getOrders(user.id).then(orders => {
    getOrderDetail(orders[0].id).then(detail => {
      console.log(detail)
    })
  })
})

这实际上是把回调地狱换了个皮,完全没有利用 Promise 链式调用的优势。

正确写法:

js
getUserInfo()
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => console.log(detail))

或使用 async/await:

js
async function loadDetail() {
  const user = await getUserInfo()
  const orders = await getOrders(user.id)
  const detail = await getOrderDetail(orders[0].id)
  console.log(detail)
}

2. 错误吞没 ​

js
const p = new Promise((resolve, reject) => {
  reject(new Error('something went wrong'))
})

如果不对 p 调用 catch,这个错误会被"吞没"——在 Node.js 中会触发 unhandledRejection 事件,在浏览器中会在控制台输出警告,但不会中断程序执行。

更隐蔽的情况:

js
Promise.resolve()
  .then(() => {
    JSON.parse('invalid json')
  })

then 回调中的同步异常会被自动捕获并变为 rejection,但如果没有后续的 catch,这个错误就会静默消失。

最佳实践:始终在 Promise 链的末尾添加 catch,或者使用全局错误处理:

js
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled rejection:', event.reason)
})

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection:', reason)
})

3. 不必要的 await ​

js
async function getData() {
  const data = await fetch('/api/data')
  return await data.json()
}

最后一个 await 是不必要的,因为 async 函数的返回值本身就会被 Promise.resolve 包装:

js
async function getData() {
  const data = await fetch('/api/data')
  return data.json()
}

但有一个例外——如果外面有 try/catch 且需要捕获 data.json() 可能产生的异常,那么 await 是必要的:

js
async function getData() {
  try {
    const data = await fetch('/api/data')
    return await data.json()
  } catch (e) {
    return { error: e.message }
  }
}

如果不 await,data.json() 的异常不会被 try/catch 捕获,因为 Promise 在 return 之后才会 reject。

4. 在循环中错误使用 await ​

js
async function processItems(items) {
  const results = []
  for (const item of items) {
    const result = await processItem(item)
    results.push(result)
  }
  return results
}

这会导致所有请求串行执行。如果这些请求之间没有依赖关系,应该并行处理:

js
async function processItems(items) {
  const results = await Promise.all(
    items.map(item => processItem(item))
  )
  return results
}

但如果需要控制并发,则应使用前文所述的并发控制方案。

5. Promise.all 的脆弱性 ​

js
const results = await Promise.all([
  fetchUser(),
  fetchOrders(),
  fetchSettings()
])

任何一个失败都会导致整体失败,即使其他请求已经成功。如果业务允许部分失败,应该使用 Promise.allSettled:

js
const results = await Promise.allSettled([
  fetchUser(),
  fetchOrders(),
  fetchSettings()
])

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('成功:', result.value)
  } else {
    console.log('失败:', result.reason)
  }
})

6. 混淆 then 和 catch 的返回值 ​

js
Promise.reject('error')
  .catch(r => {
    console.log('caught:', r)
  })
  .then(v => {
    console.log('then:', v)
  })

输出:

caught: error
then: undefined

很多人以为 catch 之后链就终止了,但实际上 catch 也返回一个新的 Promise。如果 catch 的回调没有抛出异常,返回的 Promise 就是 fulfilled(值为 catch 回调的返回值,此处为 undefined),于是后续的 then 会正常执行。

7. 构造函数中的异步错误 ​

js
new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error('async error')
  }, 0)
})

这个错误不会被 Promise 捕获。executor 的 try/catch 只能捕获同步异常。setTimeout 回调中的异常发生在新的调用栈中,Promise 的 try/catch 早已退出。

正确做法:

js
new Promise((resolve, reject) => {
  setTimeout(() => {
    try {
      throw new Error('async error')
    } catch (e) {
      reject(e)
    }
  }, 0)
})

经典面试题 ​

面试题一:Promise 执行顺序 ​

js
console.log('start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

Promise.resolve()
  .then(() => {
    console.log('promise1')
  })
  .then(() => {
    console.log('promise2')
  })

new Promise((resolve) => {
  console.log('promise3')
  resolve()
}).then(() => {
  console.log('promise4')
})

console.log('end')

输出顺序:

start
promise3
end
promise1
promise4
promise2
setTimeout

分析:

  1. 执行同步代码:输出 start
  2. setTimeout 回调注册到宏任务队列
  3. Promise.resolve().then(...) 注册微任务 A(输出 promise1)
  4. new Promise 的 executor 同步执行,输出 promise3;resolve() 后 then 注册微任务 B(输出 promise4)
  5. 输出 end
  6. 同步代码执行完毕,清空微任务队列:先执行微任务 A(输出 promise1),微任务 A 的 then 注册微任务 C(输出 promise2),再执行微任务 B(输出 promise4),再执行微任务 C(输出 promise2)
  7. 微任务队列清空,执行宏任务:输出 setTimeout

面试题二:then 返回值与链式传递 ​

js
Promise.resolve(1)
  .then(v => {
    console.log(v)
    return v + 1
  })
  .then(v => {
    console.log(v)
    throw new Error('error')
  })
  .catch(e => {
    console.log(e.message)
    return 4
  })
  .then(v => {
    console.log(v)
  })
  .then(v => {
    console.log(v)
    return Promise.resolve(6)
  })
  .then(v => {
    console.log(v)
  })

输出顺序:

1
2
error
4
undefined
6

分析:

  1. Promise.resolve(1) → then 接收 v = 1,输出 1,返回 2
  2. 下一个 then 接收 v = 2,输出 2,抛出错误
  3. 错误冒泡到 catch,输出 error,返回 4(catch 返回的 Promise 变为 fulfilled)
  4. 下一个 then 接收 v = 4,输出 4,没有显式返回值,默认返回 undefined
  5. 下一个 then 接收 v = undefined,输出 undefined,返回 Promise.resolve(6)
  6. Promise.resolve(6) 被解包,下一个 then 接收 v = 6,输出 6

面试题三:async/await 与事件循环 ​

js
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

async1()

new Promise((resolve) => {
  console.log('promise1')
  resolve()
}).then(() => {
  console.log('promise2')
})

console.log('script end')

输出顺序:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

分析:

  1. 输出 script start
  2. setTimeout 注册宏任务
  3. 调用 async1(),输出 async1 start
  4. 执行 await async2(),先同步执行 async2(),输出 async2
  5. await 相当于 async2().then(() => { console.log('async1 end') }),将 async1 end 的输出注册为微任务 A
  6. async1 让出执行权,继续执行同步代码
  7. new Promise 的 executor 同步执行,输出 promise1;resolve() 后 then 注册微任务 B
  8. 输出 script end
  9. 同步代码执行完毕,清空微任务队列:执行微任务 A(输出 async1 end),执行微任务 B(输出 promise2)
  10. 执行宏任务:输出 setTimeout

面试题四:并发控制实战 ​

实现一个函数,接收 URL 数组和最大并发数,返回所有请求结果:

js
async function fetchWithConcurrency(urls, maxConcurrency) {
  const results = new Array(urls.length)
  let currentIndex = 0

  async function worker() {
    while (currentIndex < urls.length) {
      const index = currentIndex++
      try {
        results[index] = await fetch(urls[index]).then(r => r.json())
      } catch (e) {
        results[index] = { error: e.message }
      }
    }
  }

  const workers = Array.from(
    { length: Math.min(maxConcurrency, urls.length) },
    () => worker()
  )

  await Promise.all(workers)
  return results
}

这种实现使用了 worker 池模式:创建 maxConcurrency 个 worker,每个 worker 不断从任务队列(通过 currentIndex 隐式维护)中取任务执行,直到所有任务完成。

面试题五:Promise 实现红绿灯交替 ​

实现一个红绿灯,红灯亮 3 秒,绿灯亮 2 秒,黄灯亮 1 秒,循环交替:

js
function light(color, duration) {
  return new Promise(resolve => {
    console.log(color)
    setTimeout(resolve, duration)
  })
}

async function trafficLight() {
  while (true) {
    await light('红灯', 3000)
    await light('绿灯', 2000)
    await light('黄灯', 1000)
  }
}

trafficLight()

这道题考察的是如何用 Promise + async/await 实现重复的异步时序控制。while(true) 不会阻塞主线程,因为每次 await 都会让出执行权。


总结 ​

┌─────────────────────────────────────────────────────────────────┐
│                     Promise 知识体系                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  基础概念           规范实现              高级应用                 │
│  ┌──────────┐     ┌──────────────┐     ┌──────────────────┐    │
│  │ 三种状态   │     │ Promise A+   │     │ 并发控制          │    │
│  │ 状态不可逆 │     │ resolvePromise│     │ 调度器实现        │    │
│  │ 微任务执行 │     │ then 链      │     │ worker 池模式     │    │
│  └──────────┘     │ 异步处理      │     └──────────────────┘    │
│                    └──────────────┘                              │
│  链式调用                                 语法糖                  │
│  ┌──────────┐     静态方法               ┌──────────────────┐    │
│  │ 值穿透    │     ┌──────────────┐     │ Generator        │    │
│  │ 错误冒泡  │     │ resolve/reject│     │ 自动执行器        │    │
│  │ then/catch│     │ all/allSettled│     │ async/await 本质  │    │
│  │ finally   │     │ race/any     │     │ polyfill 实现     │    │
│  └──────────┘     └──────────────┘     └──────────────────┘    │
│                                                                 │
│  常见陷阱:嵌套 Promise | 错误吞没 | 不必要的 await               │
│           串行循环     | all 脆弱性 | 异步错误未捕获               │
└─────────────────────────────────────────────────────────────────┘

Promise 的设计哲学是将异步操作的结果从时间维度中解耦出来——不管异步操作何时完成,我们都可以用统一的方式来处理它的结果。理解 Promise 的内部实现,不仅有助于在面试中脱颖而出,更重要的是能在日常开发中写出更健壮、更高效的异步代码。

用心学习,用代码说话 💻