一、前言
在JavaScript的异步编程世界里,Promise是实现复杂逻辑的基石,但是相信大部分同学都没有深入了解过它的实现细节,虽然大多数情况下我们不会在面试中遇到类似手写promise这样的题目,但是通过实现自己的promise我们可以更好地提升自己对于异步编程的理解和编码功力。因此本文会带领大家从零开始,一步一步按照 PromiseA+ 规范实现一个自己的 Promise 类并通过官方测试工具 promises-aplus-test 来进行验证,那么话不多说,让我们开始吧~
二、代码实现
为了使思路更加清晰,减少模板代码的数量,本文中的Promise将使用 ES6 class 进行实现,同时我会将整个实现过程按照功能点进行拆分,每个部分会尽量按照:原生Promise使用方法 -> 规范要求 -> 代码实现 的顺序进行编写,如果大家发现有跟不上的地方,可以反复多看几遍或者返回上一个部分进行复习~
1. 定义初始结构
我们先来复习一下ES6中的Promise是如何初始化的
const p0 = new Promise((resolve, reject) => {
})
console.log(p0) // Promise {<pending>}
const p1 = new Promise((resolve, reject) => {
resolve("Resolved");
reject("Rejected");
})
console.log(p1) // Promise {<fulfilled>: 'Resolved'}
const p2 = new Promise((resolve, reject) => {
reject("Rejected");
resolve("Resolved");
})
console.log(p2) // Promise {<rejected>: 'Rejected'}
const p3 = new Promise((resolve, reject) => {
throw new Error("Opps");
})
console.log(p3) // Promise {<rejected>: Error: Opps}
这里一共包含了两个知识点:首先是Promise类的初始化参数是一个函数,里面包含两个方法resolve和reject,它们被用于改变promise实例的状态。其次就是promise实例一共有三种状态,分别是pending(未决议),fulfilled(已完成)和rejected(被拒绝)。这里我们看一下规范中对于promise的状态如何定义的:
A promise must be in one of three states: pending, fulfilled, or rejected.
简单来说就是promise实例创建后初始状态是pending,此时可以通过resolve或reject方法改变实例的状态为fulfilled或rejected,但是这种改变是不可逆的且只有第一次生效,状态转移图如下:
除此之外还有一个小细节,那就是初始化过程中如果抛出异常e,则promise会以e为原因被拒绝。
OK,有了以上信息我们就可以开始着手搭建Promise类的架子了,代码如下:
class MyPromise {
// 状态枚举值
static Pending = "pending";
static Fulfilled = "fulfilled";
static Rejected = "rejected";
constructor(fn) {
// 初始状态为 pending
this.state = MyPromise.Pending;
// 存储 promise 的最终值
this.result = null;
try {
// 执行初始化函数, 使用 bind 避免 this 丢失
fn(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
// 初始化过程中抛出异常时拒绝 promise
this.reject(error);
}
}
resolve(value) {
// 只有pending状态下调用有效
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Fulfilled;
this.result = value;
}
}
reject(reason) {
// 只有pending状态下调用有效
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Rejected;
this.result = reason;
}
}
}
这里有几个细节需要注意下:
- 调用初始化函数fn时需要用
try...catch包裹并在接收到异常时拒绝promise - 传给fn的参数
resolve和reject需要使用bind和实例进行绑定,否则会出现this丢失
至此Promise的初始化工作就做好了,怎么样是不是很简单呢?那接下来我们就要编写promise的核心方法then了,大家可要跟紧了~
2. 实现 then
首先我们还是先来看一下ES6中的原生Promise是什么样的:
let p1 = new Promise((resolve, reject) => {
resolve('Resolved 1');
resolve('Resolved 2')
reject('Rejected');
})
p1.then(
result => {
console.log('fulfilled', result);
},
reason => {
console.log('rejected', reason.message);
}
)
// 控制台输出 "fulfilled Resolved 1"
从这里我们可以看到then方法主要的作用就是用来注册promise实例状态改变回调函数的。接下来我们跟着规范一起看看then方法有哪些实现上需要注意的细节:
A promise’s
thenmethod accepts two arguments:promise.then(onFulfilled, onRejected)
首先规定了then方法有两个可选参数分别对应成功回调和失败回调,如果有一个参数未提供,就忽略掉它。注意这里的忽略并不是指什么也不做,对于onFulfilled方法来说就是将value原封不动的返回;对于onRejected来说就是返回reason,onRejected因为是错误分支,我们返回reason应该throw一个Error,至于为什么要这么做,后面就理解了,我们继续往后看:
If
onFulfilledis a function:
这一段主要描述的是对两个回调函数自身和调用时机的要求,基本也在上面的例子中有所体现,没有什么新东西,所以接下来我们就可以动手修改我们的代码了:
class MyPromise {
// 省略其他部分...
+ then(onFulfilled, onRejected) {
+ onFulfilled =
+ typeof onFulfilled === "function" ? onFulfilled : (value) => value;
+ onRejected =
+ typeof onRejected === "function" ? onRejected : (reason) => reason;
+ if (this.state === MyPromise.Fulfilled) {
+ onFulfilled(this.state)
+ }
+ if (this.state === MyPromise.Rejected) {
+ onRejected(this.state)
+ }
+ }
}
3. 实现异步
到目前为止,我们的代码中还没有任何关于异步相关的逻辑,所以接下来的任务就是实现异步逻辑,规范中是这样定义的:
onFulfilledoronRejectedmust not be called until the execution context stack contains only platform code. [3.1].
这句话的想要表达的意思就是:成功或失败的回调函数只能通过JS引擎来调用,或者说必须通过事件循环队列来调度。
JavaScript作为一门单线程语言,其异步能力是通过事件循环队列实现的,由于事件循环并非文章重点,因此这里就不再过多赘述了,有兴趣的同学可以看下这个视频 【熟肉 | 内核】深入JavaScript中的EventLoop_哔哩哔哩_bilibili
理论上讲,promise回调事件队列的实现可以使用宏任务也可以使用微任务,这里我们与ES6中的原生Promise保持一致,使用 queueMicrotask 方法创建微任务来实现回调事件队列,修改代码如下:
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function" ? onRejected : (reason) => reason;
if (this.state === MyPromise.Fulfilled) {
+ queueMicrotask(() => {
+ onFulfilled(this.state)
+ })
}
if (this.state === MyPromise.Rejected) {
+ queueMicrotask(() => {
+ onRejected(this.state)
+ })
}
}
4. 实现then的多次调用
在ES6中,then方法可以被多次调用,用来注册多个回调函数:
const p = new Promise((resolve, reject) => {
resolve("Resolved");
})
p.then((value) => {console.log("callback 1", val)});
p.then((value) => {console.log("callbakc 2", val)});
// callback 1 Resolved
// callback 2 Resolved
对应规范内容如下:
thenmay be called multiple times on the same promise.
也就是说同一个promise实例,可以反复调用then来注册回调函数,当回调函数被触发时,要求按照注册时的顺序依次执行回调。因此我们很容易就可以想到用数组来存储注册的回调函数,更新代码如下:
class MyPromise {
static Pending = "pending";
static Fulfilled = "fulfilled";
static Rejected = "rejected";
constructor(fn) {
this.state = MyPromise.Pending;
this.result = null;
+ this.onFulfilledCallbacks = []; // 成功回调队列
+ this.onRejectedCallbacks = []; // 失败回调队列
try {
fn(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
resolve(value) {
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Fulfilled;
this.result = value;
// 依次执行成功回调
+ this.onFulfilledCallbacks.forEach((cb) => {
+ cb();
+ });
}
}
reject(reason) {
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Rejected;
this.result = reason;
// 依次执行失败回调
+ this.onRejectedCallbacks.forEach((cb) => {
+ cb();
+ });
}
}
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function" ? onRejected : (reason) => reason;
if (this.state === MyPromise.Fulfilled) {
queueMicrotask(() => {
onFulfilled(this.state);
});
}
if (this.state === MyPromise.Rejected) {
queueMicrotask(() => {
onRejected(this.state);
});
}
+ if (this.state === MyPromise.Pending) {
// 添加成功回调
+ this.onFulfilledCallbacks.push(() => {
+ queueMicrotask(() => onFulfilled(this.result));
+ });
// 添加失败回调
+ this.onRejectedCallbacks.push(() => {
+ queueMicrotask(() => onRejected(this.result));
+ });
+ }
}
}
5. 实现then的链式调用
OK,难点来了!ES6中的Promise是支持链式调用的,也就是说then方法返回的对象也应该是一个promise对象,例如下面的例子:
let p1 = new Promise((resolve, reject) => {
resolve(10)
})
p1.then(res => {
console.log('fulfilled', res);
return 2 * res
}).then(res => {
console.log('fulfilled', res)
})
// fulfilled 10
// fulfilled 20
老样子,我们来看看规范中是如何描述的:
thenmust return a promise [3.3].promise2 = promise1.then(onFulfilled, onRejected);
- If either
onFulfilledoronRejectedreturns a valuex, run the Promise Resolution Procedure[[Resolve]](promise2, x).- If either
onFulfilledoronRejectedthrows an exceptione,promise2must be rejected witheas the reason.- If
onFulfilledis not a function andpromise1is fulfilled,promise2must be fulfilled with the same value aspromise1.- If
onRejectedis not a function andpromise1is rejected,promise2must be rejected with the same reason aspromise1.
2,3,4 三条规则比较好理解,分别描述了回调函数执行过程中发生异常或回调函数缺失时,如何设置新返回的promise2对象的状态。而第一条就不是那么好理解了,由于then方法中return的值可能是一个thenable类型的值,这时我们需要对这个返回值执行一个[[Resolve]]操作,目的是提取出这个thenable的返回值。
这里我们先暂停解释下thenable的概念,thenable顾名思义就是实现了then方法的对象或者函数,由于promiseA+规范只是约定了promise类应该遵循的的行为和数据结构,没有规定实现方式,为了使我们自己实现的promise和其他人实现的实例间相互兼容,所以有了如下约定:
如果一个对象obj拥有then方法且看上去像一个 promise,我们的promise就应当尝试将obj视为一个我们自己实现的promise实例,复用obj的值和状态而不是将它视为一个普通对象。
这种 thenable 的特性使得 Promise 的实现更具有通用性,可以兼容各种不同的实现方式。
thenable概念和[[Resolve]]操作是promise实现过程中最难的部分,建议大家多看几遍规范原文,完全理解后再继续后面的编码。
这里我们暂时不考虑[[Resolve]]操作,先根据上面的规范,对我们的代码进行完善:
then(onFulfilled, onRejected) {
- onFulfilled =
- typeof onFulfilled === "function" ? onFulfilled : (value) => value;
- onRejected =
- typeof onRejected === "function" ? onRejected : (reason) => reason;
// then方法返回值必须是一个promise
+ const promise2 = new MyPromise((resolve, reject) => {
if (this.state === MyPromise.Fulfilled) {
queueMicrotask(() => {
// 回调执行异常时,返回的 promise 状态为 rejected
+ try {
+ if (typeof onFulfilled === "function") {
+ const x = onFulfilled(this.result);
// resolvePromise 操作
+ resolvePromise(promise2, x, resolve, reject);
+ } else {
// onFulfilled 不为函数时,新 promise 的值与状态复用当前实例的
+ resolve(this.result);
+ }
+ } catch (error) {
+ reject(error);
+ }
});
}
if (this.state === MyPromise.Rejected) {
queueMicrotask(() => {
// 回调执行异常时,返回的 promise 状态为 rejected
+ try {
+ if (typeof onRejected === "function") {
+ const x = onRejected(this.result);
+ resolvePromise(promise2, x, resolve, reject);
+ } else {
// onRejected 不为函数时,新 promise 的值与状态复用当前实例的
+ reject(this.result);
+ }
+ } catch (error) {
+ reject(error);
+ }
});
}
if (this.state === MyPromise.Pending) {
this.onFulfilledCallbacks.push(() => {
queueMicrotask(() => {
// 回调执行异常时,返回的 promise 状态为 rejected
try {
+ if (typeof onFulfilled === "function") {
+ const x = onFulfilled(this.result);
+ resolvePromise(promise2, x, resolve, reject);
+ } else {
// onFulfilled 不为函数时,新 promise 的值与状态复用当前实例的
+ resolve(this.result);
+ }
+ } catch (error) {
+ reject(error);
+ }
});
});
this.onRejectedCallbacks.push(() => {
queueMicrotask(() => {
// 回调执行异常时,返回的 promise 状态为 rejected
+ try {
+ if (typeof onRejected === "function") {
+ const x = onRejected(this.result);
+ resolvePromise(promise2, x, resolve, reject);
+ } else {
// onRejected 不为函数时,新 promise 的值与状态复用当前实例的
+ reject(this.result);
+ }
+ } catch (error) {
+ reject(error);
+ }
});
});
}
});
return promise2;
}
现在只剩下[[Resolve]]对应的resolvePromise方法没有实现了,接下来就让我们来一起会会它。
6. 实现[[Resolve]]抽象方法
首先让我们来看下这个[[Resolve]]方法到底做了哪些事情,下面是一个原生Promise的例子:
const a = new Promise((resolve) => {resolve(10)});
const b = new Promsie((resolve) => {resolve(20)});
const c = a.then(val => {return b});
c.then(val => val); // Promise {<fulfilled>: 20}
我们发现 a.then 中返回的是一个thenable对象 b ,而产生的新实例 c 中接收到的最终值是 b 携带的值 20 而不是thenable对象本身。这里就是[[Resolve]]方法发挥作用的地方了,它会尝试对thenable对象进行递归解封,直到获取到最内层的非thenable值。
实际上,如果只考虑我们自己的promise类型对象,我们只需要通过instanceof操作符判断实例是否属于我们自己的Promise类,如果是就递归调用[[Resolve]]方法直到获取到非thenable的返回值为止。但是为了兼容其他thenable对象,我们还需要编写一系列兼容代码,而这也正是[[Resolve]]方法麻烦的地方。
在上一节中,我们定义了[[Resolve]]方法的格式,这里先详细说明一下各个参数的意义:
/**
* @param {*} newPromise then方法返回的 promise2 对象
* @param {*} x onFulfilled方法的返回值
* @param {*} resolve promise2 对象的resolve方法
* @param {*} reject promise2 对象的reject方法
*/
function resolvePromise(newPromise, x, resolve, reject) {}
promiseA+规范中对[[Resolve]]方法的实现有着详细的描述,虽然看起来很多,但是其实只要跟着步骤一步一步来,基本上不会遇到太大的问题,我们一条一条来看:
If
promiseandxrefer to the same object, rejectpromisewith aTypeErroras the reason.
如果 promise 和 x 是同一个对象时,为了防止出现无限递归的情况,这里要抛出一个TypeError
function resolvePromise(newPromise, x, resolve, reject) {
+ if (newPromise === x) {
+ throw new TypeError("Chaining cycle detected for promise");
+ }
}
接下来我们看 x 是一个promise实例的情况:
If
xis a promise, adopt its state [3.4]:If
xis pending,promisemust remain pending untilxis fulfilled or rejected.If/when
xis fulfilled, fulfillpromisewith the same value.If/when
xis rejected, rejectpromisewith the same reason.
这里要求 newPromise 复用 x 的状态和返回值,因此我们调用 x.then ,并针对两种回调分别进行处理:
function resolvePromise(newPromise, x, resolve, reject) {
if (newPromise === x) {
throw new TypeError("Chaining cycle detected for promise");
}
+ else if (x instanceof MyPromise) {
// onFulfilled 回调时递归调用 resolvePromise,onReject 回调时直接复用 newPromise 的 reject
+ x.then((y) => resolvePromise(newPromise, y, resolve, reject), reject);
+ }
}
最后就是 x 是thanable对象和其他值的情况了,我们先来看前者:
Otherwise, if
xis an object or functionLet
thenbex.then.If retrieving the property
x.thenresults in a thrown exceptione, rejectpromisewitheas the reason.
如果 x 是一个thenable对象,我们会尝试获取它的then属性,如果获取过程中发生异常 e 则调用reject拒绝 newPromise
function resolvePromise(newPromise, x, resolve, reject) {
if (newPromise === x) {
throw new TypeError("Chaining cycle detected for promise");
}
if (x instanceof MyPromise) {
x.then((y) => resolvePromise(newPromise, y, resolve, reject), reject);
}
+ else if (x !== null && (typeof x === "object" || typeof x === "function")) {
+ let then;
+ try {
+ then = x.then;
+ } catch (error) {
+ reject(error);
+ }
+ }
}
这里解释下为什么访问 x.then 时有可能发生异常,这是由于 x 对象上的 then 有可能是一个Getter 方法,或者 x 本身是一个Proxy对象并且对get方法进行了拦截,此时x.then会调用Getter或代理函数,这个过程中是有可能出现异常的。
拿到then方法后我们就可以尝试解析thenable对象的状态和返回值了:
If
thenis a function, call it withxasthis, first argumentresolvePromise, and second argumentrejectPromise, where:
If/when
resolvePromiseis called with a valuey, run[[Resolve]](promise, y).If/when
rejectPromiseis called with a reasonr, rejectpromisewithr.If both
resolvePromiseandrejectPromiseare called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
这里别看洋洋洒洒一大段,其实核心逻辑就是一个:"尝试调用then方法并且处理两种回调"
function resolvePromise(newPromise, x, resolve, reject) {
if (newPromise === x) {
throw new TypeError("Chaining cycle detected for promise");
}
if (x instanceof MyPromise) {
x.then((y) => resolvePromise(newPromise, y, resolve, reject), reject);
}
else if (x !== null && (typeof x === "object" || typeof x === "function")) {
let then;
try {
then = x.then;
} catch (error) {
reject(error);
}
+ if (typeof then === "function") {
// resolve 和 reject 只有第一次调用时有效
+ let isCalled = false;
+ try {
+ then.call(
+ x,
// 处理 onFulfilled 回调
+ (y) => {
+ if (!isCalled) {
+ isCalled = true;
// 递归调用 resolvePromise
+ resolvePromise(newPromise, y, resolve, reject);
+ }
+ },
// 处理 onRejected 回调
+ (r) => {
+ if (!isCalled) {
+ isCalled = true;
+ reject(r);
+ }
+ }
+ );
+ } catch (error) {
// 处理 then 调用异常
+ if (!isCalled) {
+ isCalled = true;
+ reject(error);
+ }
+ }
+ }
}
}
到这里基本已经完成了,还剩下 x.then 不是函数以及 x 不是thenable对象两种情况,轻轻松松~
If
thenis not a function, fulfillpromisewithx.If
xis not an object or function, fulfillpromisewithx.
修改代码如下:
function resolvePromise(newPromise, x, resolve, reject) {
if (newPromise === x) {
throw new TypeError("Chaining cycle detected for promise");
}
if (x instanceof MyPromise) {
x.then((y) => resolvePromise(newPromise, y, resolve, reject), reject);
}
else if (x !== null && (typeof x === "object" || typeof x === "function")) {
let then;
try {
then = x.then;
} catch (error) {
reject(error);
}
if (typeof then === "function") {
let isCalled = false;
try {
then.call(
x,
(y) => {
if (!isCalled) {
isCalled = true;
resolvePromise(newPromise, y, resolve, reject);
}
},
(r) => {
if (!isCalled) {
isCalled = true;
reject(r);
}
}
);
} catch (error) {
if (!isCalled) {
isCalled = true;
reject(error);
}
}
+ } else {
+ resolve(x);
+ }
+ } else {
+ resolve(x);
+ }
}
大功告成!下面是完整代码供大家参考
class MyPromise {
static Pending = "pending";
static Fulfilled = "fulfilled";
static Rejected = "rejected";
constructor(fn) {
this.state = MyPromise.Pending;
this.result = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
try {
fn(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
resolve(value) {
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Fulfilled;
this.result = value;
this.onFulfilledCallbacks.forEach((cb) => {
cb();
});
}
}
reject(reason) {
if (this.state === MyPromise.Pending) {
this.state = MyPromise.Rejected;
this.result = reason;
// 依次执行失败回调
this.onRejectedCallbacks.forEach((cb) => {
cb();
});
}
}
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === MyPromise.Fulfilled) {
queueMicrotask(() => {
try {
if (typeof onFulfilled === "function") {
const x = onFulfilled(this.result);
resolvePromise(promise2, x, resolve, reject);
} else {
resolve(this.result);
}
} catch (error) {
reject(error);
}
});
}
if (this.state === MyPromise.Rejected) {
queueMicrotask(() => {
try {
if (typeof onRejected === "function") {
const x = onRejected(this.result);
resolvePromise(promise2, x, resolve, reject);
} else {
reject(this.result);
}
} catch (error) {
reject(error);
}
});
}
if (this.state === MyPromise.Pending) {
this.onFulfilledCallbacks.push(() => {
queueMicrotask(() => {
try {
if (typeof onFulfilled === "function") {
const x = onFulfilled(this.result);
resolvePromise(promise2, x, resolve, reject);
} else {
resolve(this.result);
}
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
queueMicrotask(() => {
try {
if (typeof onRejected === "function") {
const x = onRejected(this.result);
resolvePromise(promise2, x, resolve, reject);
} else {
reject(this.result);
}
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
}
}
function resolvePromise(newPromise, x, resolve, reject) {
if (newPromise === x) {
throw new TypeError("Chaining cycle detected for promise");
}
if (x instanceof MyPromise) {
x.then((y) => {
resolvePromise(newPromise, y, resolve, reject);
}, reject);
} else if (x !== null && (typeof x === "object" || typeof x === "function")) {
let then;
try {
then = x.then;
} catch (error) {
reject(error);
}
if (typeof then === "function") {
let isCalled = false;
try {
then.call(
x,
(y) => {
if (!isCalled) {
isCalled = true;
resolvePromise(newPromise, y, resolve, reject);
}
},
(r) => {
if (!isCalled) {
isCalled = true;
reject(r);
}
}
);
} catch (error) {
if (!isCalled) {
isCalled = true;
reject(error);
}
}
} else {
resolve(x);
}
} else {
resolve(x);
}
}
接下来我们可以使用官方提供的测试集来测试下我们的Promise类,首先安装测试库相关依赖:
npm i promises-aplus-tests
接下来在我们的代码文件中添加如下代码:
MyPromise.deferred = function () {
let result = {};
result.promise = new MyPromise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
};
module.exports = MyPromise;
然后前往pacakge.json添加脚本命令(假设文件路径为src/index.js):
{
"scripts": {
"test": "promises-aplus-tests src/index.js"
}
}
最后运行命令行:
npm run test
运行结果:
可以看到872个测试用例全部通过,完美收官~