技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

BAT 前端经典面试问题:手写 Promise 教程

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/13-BAT前端经典面试问题:史上最最最详细的手写Promise教程
本文目录13 个章节
  1. 一、Promise 解决什么问题
  2. 二、最基本的 Promise 声明
  3. 三、Promise 的三种状态
  4. 四、从状态到异步 handler
  5. 五、链式调用与 promise2
  6. 六、Promise/A+ 的 resolvePromise
  7. 七、一个完整的教学实现
  8. 八、测试 thenable 和状态锁
  9. 九、静态方法的边界
  10. 十、Promise/A+ 测试适配器
  11. 十一、setTimeout 与原生 Promise 时序
  12. 十二、总结
  13. 参考资料

BAT 前端经典面试问题:手写 Promise 教程

Category(分类): JavaScript Status: 已更新

原文作者:Carlus 历史文章:BAT 前端经典面试问题:手写 Promise 教程

原文按照“声明 Promise → 状态 → 异步回调 → 链式调用 → resolvePromise → 静态方法 → A+ 测试”的顺序展开。本文保留这条学习路线,修复抓取造成的注释粘连、Promise.all 作用域错误、普通值/thenable 处理缺失、状态锁不足和语法问题。示例类命名为 MyPromise,避免覆盖原生 Promise

一、Promise 解决什么问题

Promise 表示一个最终可能兑现或拒绝的异步操作。它把“未来的结果”和“如何处理结果”分开:

fetch('/api/user')
  .then(response => response.json())
  .then(user => console.log(user))
  .catch(error => console.error(error))

Promise/A+ 主要规定了 .then() 的互操作和链式解析行为;ECMAScript 原生 Promise 还规定了构造器、thenable 吸收、静态方法、微任务时序等内容。手写 Promise 时必须说明自己实现的是哪一部分。

原文使用 axios、fetch 等库作为例子;它们都返回或使用 Promise,但 Promise 本身不执行网络请求,也没有通用取消方法。

二、最基本的 Promise 声明

原生写法接收一个 executor,并在构造时立即调用它:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 100)
})

promise.then(value => {
  console.log(value)
})

手写实现至少需要:

  • pendingfulfilledrejected 三种状态;
  • executor 构造时同步执行;
  • resolvereject 只能让 Promise 最终完成一次;
  • executor 抛出的异常会使 Promise 拒绝;
  • then() 返回新的 Promise,支持链式调用;
  • handler 必须异步执行。

原文:Promise/A+ 资料梗图

三、Promise 的三种状态

状态只能沿一个方向变化:

pending ── resolve ──> fulfilled
pending ── reject  ──> rejected

一旦 Promise 被兑现或拒绝,之后的 resolve/reject 调用不会再次改变它。注意:原生 Promise 的 resolve(thenable) 会先把 Promise 标记为“已解决但可能仍 pending”,再吸收 thenable 的最终状态。因此,只有检查 state === 'pending' 还不够,还需要区分“resolve/reject 函数是否已经被调用”和“最终状态是否已经落定”。

四、从状态到异步 handler

如果 then() 调用时 Promise 已经完成,可以安排 handler;如果仍 pending,就先保存 handler,待状态落定后再安排:

const enqueue = typeof queueMicrotask === 'function'
  ? queueMicrotask
  : callback => Promise.resolve().then(callback)

class BasicPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('executor 必须是函数')
    }

    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.handlers = []

    const fulfill = value => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.flush()
    }

    const reject = reason => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.reason = reason
      this.flush()
    }

    try {
      executor(fulfill, reject)
    } catch (error) {
      reject(error)
    }
  }

  flush() {
    const handlers = this.handlers.splice(0)
    for (const handler of handlers) {
      enqueue(handler)
    }
  }

  then(onFulfilled, onRejected) {
    const handler = () => {
      if (this.state === 'fulfilled') {
        onFulfilled?.(this.value)
      } else {
        onRejected?.(this.reason)
      }
    }

    if (this.state === 'pending') {
      this.handlers.push(handler)
    } else {
      enqueue(handler)
    }

    return this
  }
}

这个阶段只是说明“保存回调并在状态改变后执行”的思路,还不支持真正的链式返回,也没有 thenable 吸收。

五、链式调用与 promise2

Promise/A+ 要求:

const promise2 = promise1.then(onFulfilled, onRejected)

then() 必须返回一个新的 Promise。回调的返回值记为 x,新的 promise2 应该根据 x 决定最终结果:

  • x 是普通值:兑现 promise2
  • x 是 Promise 或 thenable:等待它的最终结果;
  • 回调抛错:拒绝 promise2
  • x === promise2:拒绝并报告链式循环。
const promise = new MyPromise(resolve => resolve(1))

const next = promise.then(value => value + 1)
next.then(value => console.log(value)) // 2

六、Promise/A+ 的 resolvePromise

下面是链式解析的核心。它只读取一次 x.then,并使用 called 防止恶意 thenable 同时调用成功和失败回调:

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

  if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
    resolve(x)
    return
  }

  let then
  try {
    then = x.then
  } catch (error) {
    reject(error)
    return
  }

  if (typeof then !== 'function') {
    resolve(x)
    return
  }

  let called = false

  try {
    then.call(
      x,
      value => {
        if (called) return
        called = true
        resolvePromise(promise2, value, resolve, reject)
      },
      reason => {
        if (called) return
        called = true
        reject(reason)
      }
    )
  } catch (error) {
    if (called) return
    called = true
    reject(error)
  }
}

这段算法描述的是链式返回值解析,不完全等于构造器里的 resolve。原生 Promise 的构造器 resolve 也会吸收 thenable,但还要处理“resolve 已调用、外层仍 pending”的状态锁。

七、一个完整的教学实现

下面的实现同时处理:

  • executor 类型检查和异常转拒绝;
  • resolve/reject 的独立状态锁;
  • 构造器中的 Promise/thenable 吸收;
  • thenable 的 then getter 异常和多次调用;
  • handler 的异步执行;
  • then 链式解析;
  • catchfinally 和常见静态方法。
const enqueue = typeof queueMicrotask === 'function'
  ? queueMicrotask
  : callback => Promise.resolve().then(callback)

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

  if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
    resolve(x)
    return
  }

  let then
  try {
    then = x.then
  } catch (error) {
    reject(error)
    return
  }

  if (typeof then !== 'function') {
    resolve(x)
    return
  }

  let called = false

  try {
    then.call(
      x,
      value => {
        if (called) return
        called = true
        resolvePromise(promise2, value, resolve, reject)
      },
      reason => {
        if (called) return
        called = true
        reject(reason)
      }
    )
  } catch (error) {
    if (called) return
    called = true
    reject(error)
  }
}

class MyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('Promise resolver is not a function')
    }

    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.handlers = []

    // 已调用 resolve/reject 的锁,与最终 state 分开。
    let locked = false

    const fulfill = value => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.flush()
    }

    const fail = reason => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.reason = reason
      this.flush()
    }

    const resolveValue = value => {
      if (value === this) {
        fail(new TypeError('A promise cannot resolve itself'))
        return
      }

      if (value !== null && (
        typeof value === 'object' || typeof value === 'function'
      )) {
        let then

        try {
          then = value.then
        } catch (error) {
          fail(error)
          return
        }

        if (typeof then === 'function') {
          // 原生 Promise 会通过 PromiseResolveThenableJob 异步调用 then。
          enqueue(() => {
            let called = false

            try {
              then.call(
                value,
                nextValue => {
                  if (called) return
                  called = true

                  if (nextValue === value) {
                    fail(new TypeError('Thenable resolved with itself'))
                    return
                  }

                  resolveValue(nextValue)
                },
                reason => {
                  if (called) return
                  called = true
                  fail(reason)
                }
              )
            } catch (error) {
              if (!called) {
                called = true
                fail(error)
              }
            }
          })
          return
        }
      }

      fulfill(value)
    }

    const resolve = value => {
      if (locked) return
      locked = true
      resolveValue(value)
    }

    const reject = reason => {
      if (locked) return
      locked = true
      fail(reason)
    }

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

  flush() {
    const handlers = this.handlers.splice(0)
    for (const handler of handlers) {
      enqueue(handler)
    }
  }

  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((resolve, reject) => {
      const run = () => {
        const callback = this.state === 'fulfilled'
          ? onFulfilled
          : onRejected
        const fallback = this.state === 'fulfilled'
          ? value => value
          : reason => { throw reason }
        const handler = typeof callback === 'function' ? callback : fallback

        try {
          const x = handler(
            this.state === 'fulfilled' ? this.value : this.reason
          )
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      }

      if (this.state === 'pending') {
        this.handlers.push(run)
      } else {
        enqueue(run)
      }
    })

    return promise2
  }

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

  finally(onFinally) {
    const callback = typeof onFinally === 'function'
      ? onFinally
      : () => undefined

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

  static resolve(value) {
    if (value instanceof this) {
      return value
    }
    return new this(resolve => resolve(value))
  }

  static reject(reason) {
    return new this((resolve, reject) => reject(reason))
  }

  static all(iterable) {
    const Constructor = this

    return new Constructor((resolve, reject) => {
      const values = Array.from(iterable)
      if (values.length === 0) {
        resolve([])
        return
      }

      const results = new Array(values.length)
      let remaining = values.length

      values.forEach((value, index) => {
        Constructor.resolve(value).then(result => {
          results[index] = result
          remaining -= 1
          if (remaining === 0) {
            resolve(results)
          }
        }, reject)
      })
    })
  }

  static race(iterable) {
    const Constructor = this

    return new Constructor((resolve, reject) => {
      for (const value of iterable) {
        Constructor.resolve(value).then(resolve, reject)
      }
    })
  }

  static allSettled(iterable) {
    const Constructor = this

    return new Constructor((resolve, reject) => {
      let values
      try {
        values = Array.from(iterable)
      } catch (error) {
        reject(error)
        return
      }

      Constructor.all(values.map(value => (
        Constructor.resolve(value).then(
          result => ({ status: 'fulfilled', value: result }),
          reason => ({ status: 'rejected', reason })
        )
      ))).then(resolve, reject)
    })
  }

  static any(iterable) {
    const Constructor = this

    return new Constructor((resolve, reject) => {
      const values = Array.from(iterable)
      if (values.length === 0) {
        reject(new AggregateError([], 'All promises were rejected'))
        return
      }

      const reasons = new Array(values.length)
      let remaining = values.length

      values.forEach((value, index) => {
        Constructor.resolve(value).then(resolve, reason => {
          reasons[index] = reason
          remaining -= 1
          if (remaining === 0) {
            reject(new AggregateError(reasons, 'All promises were rejected'))
          }
        })
      })
    })
  }

  static withResolvers() {
    let resolve
    let reject
    const promise = new this((resolveFunction, rejectFunction) => {
      resolve = resolveFunction
      reject = rejectFunction
    })
    return { promise, resolve, reject }
  }
}

实现中的关键点

  1. locked 防止 resolve(thenable) 后 executor 再调用 reject
  2. thenable 的 then 只读取一次,并通过 called 防止重复回调;
  3. 构造器 resolve 会递归吸收 thenable,而不是把 thenable 对象本身作为成功值;
  4. then() 的默认成功处理器返回原值,默认失败处理器重新抛错;
  5. promise2 在回调异步执行时已经完成初始化,所以可以用于循环检测;
  6. all() 需要处理普通值、任意可迭代对象和空输入,并按输入顺序保存结果;
  7. race([]) 永久 pending 是规范语义,all([]) 则应立即兑现为空数组;
  8. allSettled() 收集全部结果,any() 在第一个成功时兑现,全部失败时使用 AggregateError

八、测试 thenable 和状态锁

const pendingThenable = {
  then(resolve) {
    setTimeout(() => resolve(42), 10)
  }
}

const outer = new MyPromise((resolve, reject) => {
  resolve(pendingThenable)
  reject(new Error('这个 reject 应被忽略'))
})

outer.then(value => {
  console.log(value) // 42
})

恶意 thenable 也不能让 Promise 同时成功和失败:

const hostileThenable = {
  then(resolve, reject) {
    resolve('first')
    reject(new Error('ignored'))
    resolve('ignored again')
  }
}

MyPromise.resolve(hostileThenable).then(
  value => console.log(value),
  error => console.error(error)
)

链式循环会拒绝:

let next
const first = MyPromise.resolve('value')

next = first.then(() => next)
next.catch(error => {
  console.log(error instanceof TypeError) // true
})

原文:Promise 教程历史配图

九、静态方法的边界

原文的 Promise.racePromise.all 直接调用 promises[i].then,因此遇到普通数字、字符串或 thenable 时会出错。现代实现应先通过构造器的 resolve 进行 Promise 化:

MyPromise.all([1, MyPromise.resolve(2), 3]).then(values => {
  console.log(values) // [1, 2, 3]
})

MyPromise.race([
  new MyPromise(resolve => setTimeout(() => resolve('slow'), 20)),
  'fast'
]).then(value => {
  console.log(value) // fast
})

同时要注意:

  • Promise.all fail-fast,但不会自动取消其他操作;
  • Promise.race([]) 会一直 pending;
  • Promise.allSettled 等待全部完成;
  • Promise.any 只要一个成功就成功,全部失败才拒绝;
  • Promise.resolve 会吸收 thenable;
  • 原生 Promise 还涉及构造器身份、子类化、微任务时序等边界,教学实现不应声称完全等价。

十、Promise/A+ 测试适配器

promises-aplus-tests 主要验证 .then() 的 Promise/A+ 契约,不会自动验证 ECMAScript 的静态方法、构造器 thenable 吸收、原生微任务顺序或取消语义。

如果要测试 MyPromise 的 A+ 行为,可以在 Node.js 文件末尾加入适配器:

MyPromise.deferred = MyPromise.defer = function deferred() {
  const result = {}

  result.promise = new MyPromise((resolve, reject) => {
    result.resolve = resolve
    result.reject = reject
  })

  return result
}

module.exports = MyPromise

然后在项目中安装测试工具并运行:

pnpm add -D promises-aplus-tests
pnpm exec promises-aplus-tests path/to/my-promise.js

本文章不执行该测试命令;它需要项目依赖和 Node 环境。即使 A+ 测试通过,仍应额外测试:

  • 构造器 resolve 普通值、原生 Promise 和恶意 thenable;
  • all([])race([])、普通值和任意 iterable;
  • handler 抛错和错误穿透;
  • finally() 的保留值/原因行为;
  • 微任务与 setTimeout 的执行顺序;
  • 静态方法和子类化边界。

原文:Promise 教程历史配图

十一、setTimeout 与原生 Promise 时序

Promise/A+ 只要求 handler 异步调用,并不规定必须使用哪一种宿主调度机制。教学实现使用 setTimeout(..., 0) 可以满足 A+ 的“不能同步调用”要求,但它与原生 Promise 的微任务时序不同:

console.log('start')

Promise.resolve().then(() => {
  console.log('promise job')
})

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

console.log('end')

// start
// end
// promise job
// timer task

原生 Promise reaction 通常作为 Promise job/microtask 在下一个任务前执行;定时器则受任务队列和宿主调度影响。文章中的 setTimeout 实现应标记为 A+ 教学实现,不要当成原生 Promise 的完整时序模拟。

十二、总结

  • Promise 有 pendingfulfilledrejected 三种状态,最终状态只能确定一次;
  • 状态锁必须区分“resolve/reject 已调用”和“thenable 最终完成”;
  • .then() 总是返回新 Promise,返回值需要经过 thenable 解析;
  • resolvePromise 要处理循环引用、只读一次 then、多次调用和 getter 异常;
  • 构造器 resolve 也要吸收 Promise/thenable,不能只保存对象本身;
  • allraceallSettledany 要支持普通值、可迭代对象和各自的空输入语义;
  • Promise/A+ 测试只覆盖 then 契约,不代表完整 ECMAScript Promise 实现;
  • 生产代码优先使用原生 Promise,手写实现主要用于理解规范和面试讨论。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS