第 4 期 - 手写 Promise 的实现与优化
摘要
文章按照 Promise A +规范手写 Promise,详细阐述从基础构建到逐步优化的过程,包括 then 方法难点、实例方法 catch 和 finally 的实现,静态方法的手写,以及通过测试确保符合规范,还提及后续迭代器与生成器的学习预告。
一、Promise 的基本概念与规范
- Promise 的标准化历程是因为早期不同库对 Promise 的实现存在差异,导致兼容性和行为不可预测等问题。Promise A +规范应运而生,其主要目标是提供一个简单而健壮的 then 方法。该规范由社区推动,后来 ECMAScript 2015 (ES6)中的 Promise 很大程度上吸收了其精髓。
// 例如这里定义 Promise 的状态常量
const PROMISE_STATUS_PENDING = 'pending';
const PROMISE_STATUS_FULFILLED = 'fulfilled';
const PROMISE_STATUS_REJECTED = 'rejected';
二、手写 Promise 的构造函数
- 首先构建 Promise 可以选择 ES5 的 function 形式或者 ES6 的 Class 形式,这里采用 Class 形式。在构造函数中,传入的回调函数会立即被调用,同时需要实现 resolve 和 reject 逻辑。
- 改变 Promise 状态是构造函数中的一个关键部分,默认初始状态为 pending,当调用 resolve 或 reject 方法时,只有状态为 pending 才会执行状态改变。
- 另外还需要处理异步信息传递,通过声明变量来接收 resolve 和 reject 传递的值。
class MyPromise {
constructor(executor) {
this.status = PROMISE_STATUS_PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_FULFILLED;
this.value = value;
console.log("resolve 被调用");
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED;
this.reason = reason;
console.log("reject 被调用");
}
};
executor(resolve, reject);
}
}
三、then 方法的实现与优化
(一)初步实现
- then 方法的参数顺序有规范要求,并且需要注意执行顺序的问题。在初步编写 then 方法时,将回调函数存储起来,但是会遇到 this.onFulfilled 不是函数的报错。这是因为在执行 resolve 或 reject 时,then 方法可能还未执行,导致 this.onFulfilled 和 this.onRejected 未被正确赋值。
then(onFulfilled, onRejected) {
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
(二)异步执行顺序的处理
- 为了解决上述执行顺序问题,一开始使用 setTimeout 进行嵌套,但这不是好的设计方式,因为 setTimeout 是宏任务,而 Promise 执行相关操作时是微任务,所以后来使用 queueMicrotask 方法。
(三)then 方法的多次调用优化
- 当 then 方法多次调用时,需要采用数组存储每次的调用,然后统一执行。但这会遇到在 Promise 状态确定后再次调用 then 方法的执行问题,需要通过判断 Promise 的状态来确保这些调用能正确执行。
(四)then 方法的链式调用优化
- 为了使 then 方法能够链式调用,需要让 then 方法返回一个 MyPromise 实例对象。在处理返回值时,需要对不同的返回情况进行处理,包括对 Promise 和 thenable 对象的处理。还需要通过 try...catch 来捕获回调中的异常。
四、catch 方法的手写
- catch 方法用于注册在 promise 被拒绝时调用的函数,是 then(undefined, onRejected)的简写形式。其主要难点在于处理等效 Promise 的返回以及解决与 then 方法之间的边界问题,如对 onRejected 为 undefined 的情况进行处理,通过在 then 方法中先判断 onRejected 是否存在来解决。
catch(onRejected) {
return this.then(undefined, onRejected);
}
五、finally 方法的手写
- finally 方法用于注册在 promise 敲定(兑现或拒绝)时调用的函数,类似于调用 then(onFinally, onFinally)。需要注意的是,在 then 与 finally 之间间隔 catch 处理时可能出现的执行问题,通过在 then 方法中对 onFulfilled 进行判断和处理来解决。
finally(onFinally) {
this.then(() => {
onFinally();
}, () => {
onFinally();
});
}
六、Promise 静态方法的手写
(一)resolve 和 reject 方法
- resolve 和 reject 静态方法都是将传入的值包裹为 Promise 值,只是状态不同。
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason));
}
(二)all 和 allSettled 方法
- all 方法聚合多个 Promise 的结果,所有 Promise 都 fulfilled 才一起返回对应内容,中途有 rejected 直接返回 rejected 内容。allSettled 方法则是等待所有 Promise 结束,无论结果如何,通过存储对象来区分每个 Promise 的状态和结果。
static all(promises) {
return new MyPromise((resolve, reject) => {
const values = [];
promises.forEach(promise => {
promise.then(res => {
values.push(res);
if (values.length === promises.length) {
resolve(values);
}
}, err => {
reject(err);
});
});
});
}
static allSettled(promises) {
return new MyPromise((resolve) => {
const results = [];
promises.forEach(promise => {
promise.then(res => {
results.push({ status: PROMISE_STATUS_FULFILLED, value: res });
if (results.length === promises.length) resolve(results);
}, err => {
results.push({ status: PROMISE_STATUS_REJECTED, value: err });
if (results.length === promises.length) resolve(results);
});
});
});
}
(三)race 和 any 方法
- race 方法返回最先解决或拒绝的 Promise 的结果,any 方法只要有一个 Promise 解决就解决,所有都拒绝才拒绝,并且在所有都拒绝时使用 AggregateError 对象包裹错误进行返回。
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve, reject);
});
});
}
static any(promises) {
const reasons = [];
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve, err => {
reasons.push(err);
if (reasons.length === promises.length) {
reject(new AggregateError(reasons));
}
});
});
});
}
七、Promise A +规范测试
- 为了验证手写的 Promise 是否正确,需要通过 Promises/A +的测试。首先安装 promises - aplus - tests 库,然后编写适配器文件,最后运行测试文件,如果测试通过则说明手写的 Promise 符合规范。
八、后续学习预告
- 文章最后预告了下一章将学习迭代器与生成器相关知识,包括其概念、与普通函数的区别以及在实际开发中的应用等内容。
扩展阅读
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有