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')

输出顺序:12453

executor 函数(传入 new Promise 的函数)是同步执行的,所以 24 会在同步流程中按顺序输出。而 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,于是第三个 thenonRejected 被触发。

值穿透

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)
}

catchthen 的第二个参数有一个重要区别:catch 能捕获前一个 thenonFulfilled 回调中抛出的异常,而 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 不会被触发,因为异常发生在同一个 thenonFulfilled 中。第二个写法中 catch 能正确捕获异常,因为 catch 作用于 then 返回的新 Promise。

finally 的实现原理

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

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

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

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

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

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)

核心规则:

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

4. Promise 解决过程(Resolution Procedure)

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

┌─────────────────────────────────────────────────────────┐
│              [[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)
    }
  }
}

关键设计点:

  • onFulfilledCallbacksonRejectedCallbacks 是数组,因为同一个 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 => valueonRejected 不是函数时,默认为 reason => { throw reason }
  • 异步执行:使用 queueMicrotask 确保回调异步执行,符合规范要求
  • 返回新 Promisethen 总是返回 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')

输出顺序:2314

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)。

修正后的输出顺序应为:2341


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

如果不 awaitdata.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 同步执行,输出 promise3resolve()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,返回 4catch 返回的 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 同步执行,输出 promise1resolve()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 的内部实现,不仅有助于在面试中脱颖而出,更重要的是能在日常开发中写出更健壮、更高效的异步代码。

用心学习,用代码说话 💻