Promise/A+ 教学实现:从 then 到 thenable 解析
Category(分类): JavaScript Status: 已整理
原标题“完全符合 Promise/A+ 规范的 Promise 源码实现”保留作历史标题,但“通过 Promise/A+ 测试”只能证明特定 adapter 的
then行为符合 A+ 测试,不能推出构造器、finally、静态组合器和所有 ECMAScript Promise 语义都完整兼容。本文把代码定位为教学实现,不替代生产环境的原生Promise。
前言
三年多以前,曾写过一篇 Promise 实现原理(附源码)。那篇文章展示了 Promise 的基本思想,但 Promise 规范边界、thenable 吸收、状态竞态和静态方法仍有很多细节。
Promises/A+ 的核心范围主要是 then 方法及 Promise Resolution Procedure,它并不规定 Promise 构造器如何暴露、如何调度任务、如何实现 finally 或静态组合器。ECMAScript 原生 Promise 在此基础上还定义了构造器、species、Promise jobs、all/race/allSettled/any 等语义。
本文的代码重点是:
- pending、fulfilled、rejected 三种状态;
then回调异步执行并按注册顺序触发;- handler 返回值决定下一个 Promise 的状态;
- 读取 thenable 的
then一次,并以第一次 resolve/reject 为准; - self-resolution、异常传播和常用静态方法的教学实现。
代码没有实现原生 Promise 的所有内部槽、species、品牌检查和宿主未处理 rejection 行为。生产代码请使用原生 Promise。
代码实现
'use strict'
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const isFunction = (value) => typeof value === 'function'
const isObjectOrFunction = (value) => (
value !== null && (typeof value === 'object' || typeof value === 'function')
)
const enqueueJob = typeof queueMicrotask === 'function'
? queueMicrotask
: (callback) => Promise.resolve().then(callback)
class MyPromise {
constructor(executor) {
if (!isFunction(executor)) {
throw new TypeError('MyPromise resolver is not a function')
}
this._state = PENDING
this._value = undefined
this._handlers = []
// alreadyResolved 与 _state 分开:resolve(pendingThenable) 后,
// Promise 外观仍是 pending,但后续 reject 必须被忽略。
let alreadyResolved = false
const resolve = (value) => {
if (alreadyResolved) return
alreadyResolved = true
this._resolveValue(value)
}
const reject = (reason) => {
if (alreadyResolved) return
alreadyResolved = true
this._settle(REJECTED, reason)
}
try {
executor(resolve, reject)
} catch (error) {
if (!alreadyResolved) {
alreadyResolved = true
this._settle(REJECTED, error)
}
}
}
_resolveValue(value) {
if (value === this) {
this._settle(REJECTED, new TypeError('Promise cannot resolve to itself'))
return
}
if (!isObjectOrFunction(value)) {
this._settle(FULFILLED, value)
return
}
let then
try {
then = value.then
} catch (error) {
this._settle(REJECTED, error)
return
}
if (!isFunction(then)) {
this._settle(FULFILLED, value)
return
}
// ECMAScript 会通过 PromiseResolveThenableJob 调用 thenable;
// 这里使用 microtask 模拟“不会在 executor 中同步调用 thenable”。
enqueueJob(() => {
let called = false
const resolveThenable = (nextValue) => {
if (called) return
called = true
this._resolveValue(nextValue)
}
const rejectThenable = (reason) => {
if (called) return
called = true
this._settle(REJECTED, reason)
}
try {
then.call(value, resolveThenable, rejectThenable)
} catch (error) {
if (!called) {
called = true
this._settle(REJECTED, error)
}
}
})
}
_settle(state, value) {
if (this._state !== PENDING) return
this._state = state
this._value = value
const handlers = this._handlers
this._handlers = []
for (const handler of handlers) {
handler()
}
}
then(onFulfilled, onRejected) {
let resolveNext
let rejectNext
const nextPromise = new MyPromise((resolve, reject) => {
resolveNext = resolve
rejectNext = reject
})
const run = () => {
enqueueJob(() => {
const callback = this._state === FULFILLED
? onFulfilled
: onRejected
const value = this._value
if (!isFunction(callback)) {
if (this._state === FULFILLED) resolveNext(value)
else rejectNext(value)
return
}
try {
resolveNext(callback(value))
} catch (error) {
rejectNext(error)
}
})
}
if (this._state === PENDING) {
this._handlers.push(run)
} else {
run()
}
return nextPromise
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
finally(onFinally) {
if (!isFunction(onFinally)) {
return this.then(onFinally, onFinally)
}
return this.then(
(value) => MyPromise.resolve(onFinally()).then(() => value),
(reason) => MyPromise.resolve(onFinally()).then(() => {
throw reason
})
)
}
static resolve(value) {
if (value instanceof MyPromise) return value
return new MyPromise((resolve) => resolve(value))
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason))
}
static all(iterable) {
return new MyPromise((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
if (values.length === 0) {
resolve([])
return
}
const result = []
let remaining = values.length
values.forEach((value, index) => {
MyPromise.resolve(value).then(
(item) => {
result[index] = item
remaining -= 1
if (remaining === 0) resolve(result)
},
reject
)
})
})
}
static race(iterable) {
return new MyPromise((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
for (const value of values) {
MyPromise.resolve(value).then(resolve, reject)
}
// 空 iterable 会保持 pending。
})
}
static allSettled(iterable) {
return new MyPromise((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
if (values.length === 0) {
resolve([])
return
}
const result = []
let remaining = values.length
values.forEach((value, index) => {
MyPromise.resolve(value).then(
(item) => {
result[index] = { status: FULFILLED, value: item }
},
(reason) => {
result[index] = { status: REJECTED, reason }
}
).finally(() => {
remaining -= 1
if (remaining === 0) resolve(result)
})
})
})
}
static any(iterable) {
return new MyPromise((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
if (values.length === 0) {
reject(new AggregateError([], 'All promises were rejected'))
return
}
const errors = []
let rejectedCount = 0
values.forEach((value, index) => {
MyPromise.resolve(value).then(
resolve,
(reason) => {
errors[index] = reason
rejectedCount += 1
if (rejectedCount === values.length) {
reject(new AggregateError(errors, 'All promises were rejected'))
}
}
)
})
})
}
}
// Promises/A+ 测试所需的 adapter 形式。
MyPromise.deferred = function deferred() {
const result = {}
result.promise = new MyPromise((resolve, reject) => {
result.resolve = resolve
result.reject = reject
})
return result
}
module.exports = MyPromise
上面的代码是一个可读的教学版本,但仍不是原生 Promise 的完全替代品。它使用 Array.from 简化了 iterator close 和构造器 species 语义,也没有完整实现子类、Symbol.species、品牌检查、未处理 rejection 追踪等能力。
Promise Resolution Procedure
then 的关键不是直接把返回值塞进下一个 Promise,而是运行解决过程:
- 如果返回值与下一个 Promise 是同一个对象,拒绝并抛出 self-resolution 错误;
- 如果返回值是对象或函数,读取一次它的
then属性; then不是函数时,把这个对象作为普通 fulfilled 值;then是函数时,以该对象作为this调用它;- thenable 多次调用 resolve/reject 时,只采用第一次;
- 读取
then或调用 thenable 抛错时,按是否已经 settle 决定是否拒绝; - resolve 传入的值仍可能是另一个 thenable,需要继续解析。
const thenable = {
then(resolve) {
resolve(42)
resolve(100)
}
}
MyPromise.resolve(thenable).then(console.log) // 42
构造器中的第一次 resolve 也会锁定结果,即使被解析的 thenable 还处于 pending:
let resolvePending
let rejectPending
const pending = new MyPromise((resolve, reject) => {
resolvePending = resolve
rejectPending = reject
})
const result = new MyPromise((resolve, reject) => {
resolve(pending)
rejectPending = reject
throw new Error('late error')
})
// 第一次 resolve 已锁定 result;pending 尚未 settle,late error 不会覆盖它。
void result.catch(() => console.log('不会由 late error 触发'))
上面的例子重点是状态闩锁;真实代码不应把内部测试变量暴露为业务状态。
then 的链式调用
MyPromise.resolve(1)
.then((value) => value + 1)
.then((value) => {
throw new Error(`value=${value}`)
})
.catch((error) => {
console.log(error.message) // value=2
})
then总是返回一个新的 Promise;- handler 返回普通值时,下一个 Promise fulfilled;
- handler 返回 Promise 或 thenable 时,下一个 Promise 等待它;
- handler 抛异常时,下一个 Promise rejected;
- 没有提供对应 handler 时,状态和值会透传;
- Promise reaction 会异步执行,
resolve()/reject()函数本身不是“立即执行所有 then 回调”的微任务。
Promises/A+ 只要求 handler 在当前执行栈结束后异步执行,并不强制规定浏览器或 Node 必须使用某一个具体队列。现代 ECMAScript/浏览器实现通常通过 Promise job 和 microtask 接入。
finally 的边界
finally 的回调不接收 fulfilled 值或 rejection reason;它的返回值会被等待,回调抛错或返回 rejected Promise 会覆盖原结果;回调正常完成时,原来的值/原因继续传递。
MyPromise.resolve('value')
.finally(() => console.log('cleanup'))
.then(console.log) // cleanup,然后 value
MyPromise.reject(new Error('original'))
.finally(() => MyPromise.resolve('cleanup'))
.catch((error) => console.log(error.message)) // original
finally(undefined) 也应透传原结果,因此实现不能无条件执行 cb()。上面的教学实现修正了这个边界,但仍没有完整实现原生 Promise 的 species 机制。
静态组合方法的边界
MyPromise.all(iterable)等待全部 fulfilled,结果按输入顺序排列,任意一项 rejected 就 rejected;race(iterable)采用第一个 settle 的结果,空 iterable 永久 pending;allSettled(iterable)等待全部结束,结果中包含status、value或reason;any(iterable)采用第一个 fulfilled,全部 rejected 时使用AggregateError;空 iterable 直接 rejected。
静态方法的原生语义还包括调用者构造器、C.resolve、iterator close、species 和异常处理。本文用 Array.from 和硬编码的 MyPromise.resolve 说明主线,不声称是完整 polyfill。
如何运行 A+ 测试
Promises/A+ 测试需要一个带 deferred() 的 adapter。代码中的 MyPromise.deferred 是对应形式:
const promisesAPlusTests = require('promises-aplus-tests')
const MyPromise = require('./my-promise')
promisesAPlusTests(MyPromise, (error) => {
if (error) console.error(error)
else console.log('A+ adapter tests completed')
})
“通过 872 个 case”只能说明测试套件覆盖的 then/resolution adapter 场景通过,不能证明 MyPromise.all、finally、静态方法、子类和宿主调度完全等同于原生 Promise。真正发布前还要补充 thenable、late reject、iterator、跨 realm 和不同宿主的测试。
生产环境建议
- 直接使用浏览器和 Node.js 原生
Promise; - 需要取消时使用
AbortController等明确的取消协议,Promise 本身不会取消底层任务; - 通过
Error实例保留 stack,不要随意reject('failure'); - 为并发任务选择
Promise.all、allSettled或any,并处理其失败契约; - 不要因为教学代码看起来短,就覆盖
Promise.prototype或替换原生实现。
总结
Promise 的难点不在三态字段,而在 resolution procedure、thenable 吸收、一次性 settle、reaction job 和错误传播。手写实现适合学习,但“符合 A+”与“兼容 ECMAScript 原生 Promise”是两个不同的声明。
参考资料
- Promises/A+ 规范
- Promises/A+ 测试 README
- ECMAScript:Promise 对象
- ECMAScript:Promise resolve functions
- ECMAScript:Promise reaction job
- ECMAScript:Promise.prototype.finally
- MDN:Promise
- MDN:Using promises
作者:面条__ 原文链接:https://juejin.cn/post/7112639843871490061/ 来源:稀土掘金。