怎样手写一个符合 Promises/A+ 规范的 Promise?
核心是三件事:状态只能从 pending 变为 fulfilled 或 rejected 一次;then 的回调总是异步执行,并返回新的 Promise;用 Promise 解决过程处理回调返回值,识别自引用、读取 then 时的异常和 thenable 多次回调。下面的实现通过了 promises-aplus-tests 的全部 872 个用例。
参考答案
简答
- 状态只能改变一次:
resolve、reject和执行器抛错共用一个called标记,先到的生效,之后的调用全部忽略。 then返回新的 Promise。回调不是函数时直接把当前结果传下去(值穿透);回调抛错时新 Promise 拒绝。- 回调必须在当前执行栈清空后才运行,即使 Promise 已经完成。这里用
queueMicrotask,与原生 Promise 一样放入微任务队列。 - 解决过程:值是 Promise 自身时以
TypeError拒绝;值是对象或函数时只读取一次then,读取抛错就拒绝;then是函数时按 thenable 处理,它的两个回调和抛错只认第一次。 - Promises/A+ 只规定
then的行为,catch、finally、Promise.resolve和Promise.all都不在规范中。
实现
JavaScript
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
#state = PENDING;
#value = undefined;
#handlers = [];
constructor(executor) {
let called = false;
const resolve = (value) => {
if (!called) {
called = true;
this.#resolve(value);
}
};
const reject = (reason) => {
if (!called) {
called = true;
this.#settle(REJECTED, reason);
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error); // 已经 resolve 过时被 called 挡住
}
}
// Promise 解决过程,对应规范 2.3
#resolve(x) {
if (x === this) {
this.#settle(REJECTED, new TypeError('Chaining cycle detected'));
return;
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let then;
try {
then = x.then; // 只读一次,then 可能是有副作用的 getter
} catch (error) {
this.#settle(REJECTED, error);
return;
}
if (typeof then === 'function') {
let called = false; // thenable 可能多次调用回调,只认第一次
try {
then.call(
x,
(y) => {
if (!called) {
called = true;
this.#resolve(y); // y 仍可能是 thenable,递归解决
}
},
(r) => {
if (!called) {
called = true;
this.#settle(REJECTED, r);
}
},
);
} catch (error) {
if (!called) {
called = true;
this.#settle(REJECTED, error);
}
}
return;
}
}
this.#settle(FULFILLED, x);
}
#settle(state, value) {
if (this.#state !== PENDING) return;
this.#state = state;
this.#value = value;
this.#handlers.forEach((handler) => this.#run(handler));
this.#handlers = [];
}
#run({ onFulfilled, onRejected, resolve, reject }) {
queueMicrotask(() => {
const callback = this.#state === FULFILLED ? onFulfilled : onRejected;
if (typeof callback !== 'function') {
// 值穿透:把结果原样交给下一个 Promise
(this.#state === FULFILLED ? resolve : reject)(this.#value);
return;
}
try {
resolve(callback(this.#value));
} catch (error) {
reject(error);
}
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handler = { onFulfilled, onRejected, resolve, reject };
if (this.#state === PENDING) this.#handlers.push(handler);
else this.#run(handler);
});
}
}
#run 中的 resolve 是新 Promise 构造时拿到的那个,它会再次进入解决过程,所以回调返回另一个 Promise 或 thenable 时,新 Promise 会跟随它的结果。
用规范测试验证
JavaScript
const promisesAplusTests = require('promises-aplus-tests');
const adapter = {
deferred() {
let resolve;
let reject;
const promise = new MyPromise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
},
};
promisesAplusTests(adapter, (err) => {
if (err) console.error(err);
});
// 本实现在 Node.js 中运行输出:872 passing
因为实现了标准的 then,原生 Promise.resolve(myPromise) 和 await myPromise 也能把它当作 thenable 使用。
常见追问
- 为什么不能同步执行已完成 Promise 的回调?那样回调有时同步、有时异步,调用方的代码顺序会随状态变化。规范要求统一在执行栈清空后运行。
- 怎样补上
catch和finally?catch(fn)就是then(undefined, fn)。finally的回调不接收结果,执行后把原结果或原错误继续传下去。 - 静态方法怎样实现?以
Promise.all为例,见《怎样实现保持输入顺序的 Promise.all 教学版本?》。