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)
})
手写实现至少需要:
pending、fulfilled、rejected三种状态;- executor 构造时同步执行;
resolve和reject只能让 Promise 最终完成一次;- executor 抛出的异常会使 Promise 拒绝;
then()返回新的 Promise,支持链式调用;- handler 必须异步执行。

三、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 的
thengetter 异常和多次调用; - handler 的异步执行;
then链式解析;catch、finally和常见静态方法。
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 }
}
}
实现中的关键点
locked防止resolve(thenable)后 executor 再调用reject;- thenable 的
then只读取一次,并通过called防止重复回调; - 构造器 resolve 会递归吸收 thenable,而不是把 thenable 对象本身作为成功值;
then()的默认成功处理器返回原值,默认失败处理器重新抛错;promise2在回调异步执行时已经完成初始化,所以可以用于循环检测;all()需要处理普通值、任意可迭代对象和空输入,并按输入顺序保存结果;race([])永久 pending 是规范语义,all([])则应立即兑现为空数组;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.race 和 Promise.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.allfail-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的执行顺序; - 静态方法和子类化边界。

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