用 TinyPromise 演示异步链式调用(约 20 行的动机)
Category(分类): JavaScript Status: 已整理
前言
面试时经常有人要求实现一个 Promise。如果按 ECMAScript 和 Promises/A+ 的全部边界来做,可能讲到天黑也结束不了。原文用约 20 行代码展示“成功值如何沿链传递”的动机仍然很好,但那段代码不能叫 Promise polyfill,也不符合 Promises/A+:它缺少拒绝、状态锁、晚注册 handler、异常传播、缺省 handler、thenable resolution 等核心语义。
本文把实现命名为 TinyPromise,不覆盖全局 Promise,用一个可运行的教学版补上这些关键机制。它仍然不是生产 polyfill,也没有实现原生 Promise 的所有静态方法、迭代器边界、调试钩子和全部规范细节。生产代码请使用宿主原生 Promise。
成功路径教学动机
先保留原来的链式场景:第一个异步操作完成后输出 1,第二个异步操作完成后输出 2。
new TinyPromise((resolve) => {
setTimeout(() => {
resolve(1)
}, 500)
})
.then((value) => {
console.log(value)
return new TinyPromise((resolve) => {
setTimeout(() => {
resolve(2)
}, 500)
})
})
.then(console.log)
典型表现是约 500ms 后输出 1,再约 500ms 后输出 2。重点不在于行数,而在于:then() 返回一个新的 Promise;handler 返回的值会决定这个新 Promise 的结果;如果 handler 返回 thenable,新 Promise 会采用它的状态。
TinyPromise 教学版
const enqueue = typeof queueMicrotask === 'function'
? queueMicrotask
: (fn) => setTimeout(fn, 0)
class TinyPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor 必须是函数')
}
this.state = 'pending'
this.value = undefined
this.handlers = []
let locked = false
const fulfill = (value) => {
if (this.state !== 'pending') return
this.state = 'fulfilled'
this.value = value
this.flush()
}
const rejectState = (reason) => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.value = reason
this.flush()
}
// 这是教学版的 resolution procedure 核心:不使用 instanceof 判断。
// seen 是额外的教学防护;它不等同于 ECMAScript 对所有间接 thenable
// 环的处理,生产代码不应据此声称与原生 Promise 完全一致。
const resolveValue = (value, seen = new Set()) => {
if (value === this) {
rejectState(new TypeError('Promise 不能解析为自身'))
return
}
if (
value !== null &&
(typeof value === 'object' || typeof value === 'function')
) {
if (seen.has(value)) {
rejectState(new TypeError('检测到 thenable 循环'))
return
}
let then
try {
then = value.then
} catch (error) {
rejectState(error)
return
}
if (typeof then === 'function') {
seen.add(value)
enqueue(() => {
let called = false
const resolveThenable = (nextValue) => {
if (called) return
called = true
resolveValue(nextValue, seen)
}
const rejectThenable = (reason) => {
if (called) return
called = true
rejectState(reason)
}
try {
then.call(value, resolveThenable, rejectThenable)
} catch (error) {
if (!called) rejectThenable(error)
}
})
return
}
}
fulfill(value)
}
const resolve = (value) => {
// resolved 与 settled 不完全相同:解析 thenable 时已锁定,但仍可能 pending。
if (locked) return
locked = true
resolveValue(value)
}
const reject = (reason) => {
if (locked) return
locked = true
rejectState(reason)
}
try {
// executor 是同步调用的;它的异常会变成 rejection。
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
flush() {
const handlers = this.handlers.splice(0)
for (const handler of handlers) {
enqueue(handler)
}
}
then(onFulfilled, onRejected) {
return new TinyPromise((resolve, reject) => {
const run = () => {
const handler = this.state === 'fulfilled'
? onFulfilled
: onRejected
if (typeof handler !== 'function') {
const pass = this.state === 'fulfilled' ? resolve : reject
pass(this.value)
return
}
try {
// resolve 会处理普通值、thenable 和 self-resolution。
resolve(handler(this.value))
} catch (error) {
reject(error)
}
}
if (this.state === 'pending') {
this.handlers.push(run)
} else {
enqueue(run)
}
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
}
代码中保留的核心状态
pending:尚未完成,handler 暂存于handlers。fulfilled:成功完成,value保存兑现值。rejected:失败完成,value保存拒绝原因。locked:第一次调用公开的resolve或reject后锁定解析权。resolve(thenable)时,TinyPromise 可能仍是pending,直到 thenable 最终完成。flush():settled 后取出已有 handler 并异步调度;settled 之后再调用then()也会异步执行,不会永远丢失。
解析返回值,而不是判断实例类型
then() 的 handler 可能返回普通值、原生 Promise、另一个 TinyPromise 或任意 thenable。下面的 thenable 没有继承任何 Promise 类,但仍会被吸收:
new TinyPromise((resolve) => {
resolve({
then(resolveThenable) {
resolveThenable(42)
},
})
}).then((value) => {
console.log(value)
})
因此不能用 value instanceof Promise 代替 resolution procedure。instanceof 无法覆盖跨 realm Promise、其他实现和普通 thenable;规范语义是读取一次 then,调用它,并用一次性守卫处理 resolve/reject。
缺省 handler、异常和拒绝传播
new TinyPromise((resolve) => {
resolve(1)
})
.then((value) => value + 1)
.then(() => {
throw new Error('handler failed')
})
.catch((error) => {
console.log(error.message)
})
new TinyPromise((resolve, reject) => {
reject(new Error('network failed'))
})
.then(undefined, (error) => {
console.log(error.message)
return 'recovered'
})
.then(console.log)
缺省的 fulfillment handler 会透传值,缺省的 rejection handler 会透传原因;handler 抛出的异常会使下一个 TinyPromise rejected。第一次 resolve 或 reject 胜出,后续调用不会再次改变状态或重复执行 handler。
晚注册和 self-resolution
const late = new TinyPromise((resolve) => {
resolve('late value')
})
setTimeout(() => {
late.then(console.log)
}, 0)
let chained
chained = new TinyPromise((resolve) => {
resolve(1)
}).then(() => chained)
chained.catch((error) => {
console.log(error.message)
})
第一个例子验证 settled 后注册的 handler 仍会被调度;第二个例子验证链不能解析为自身。教学版还用 Set 防止简单 thenable 环;这是额外的教学防护,不是原生 Promise 对所有间接 thenable 环的等价保证。复杂恶意 thenable、调度细节和所有规范边界仍应交给成熟实现。
调度说明
代码优先使用 queueMicrotask(),没有该 API 时退回 setTimeout(fn, 0)。退回方案只保证“不是当前同步调用栈立即执行”,不等同于原生 Promise reaction 的 microtask:timer 是宿主 task,可能与浏览器渲染、Node 的 nextTick/V8 microtask 和其他 I/O 有不同顺序。
Promises/A+ 要求 handler 异步执行并规定 then 与 Promise Resolution Procedure 的核心契约,但它不覆盖 ECMAScript Promise 构造器的所有静态 API,也不单独规定浏览器/Node 的调度队列。要声称实现符合 A+,应补齐边界并运行官方测试,而不是只看成功路径能打印 1、2。
总结
这段 TinyPromise 适合课堂演示以下概念:
- executor 同步执行,handler 异步调度。
pending、fulfilled、rejected三态不可逆。- 第一次
resolve/reject锁定解析权。 - settled 后注册的
then仍然会运行。 - 缺省 handler 透传值或原因,handler 异常进入拒绝链。
- 返回值通过 thenable resolution procedure 处理,并拒绝 self-resolution。
它仍然不是 Promise polyfill,不应覆盖全局 Promise,也不应在生产代码替代原生 Promise。原作者“用很少代码解释异步链式调用”的教学动机保留;实现名称和免责声明用于防止读者把示例误当成完整 A+ 库。
原文归属
作者:ssh_晨曦时梦见兮。历史来源:掘金原文《最简实现Promise,支持异步链式调用(20行)》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。