一、前言
在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
then
method accepts two arguments:promise.then(onFulfilled, onRejected)
首先规定了then
方法有两个可选参数分别对应成功回调和失败回调,如果有一个参数未提供,就忽略掉它。注意这里的忽略并不是指什么也不做,对于onFulfilled
方法来说就是将value
原封不动的返回;对于onRejected
来说就是返回reason
,onRejected
因为是错误分支,我们返回reason
应该throw
一个Error
,至于为什么要这么做,后面就理解了,我们继续往后看:
If
onFulfilled
is 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. 实现异步
到目前为止,我们的代码中还没有任何关于异步相关的逻辑,所以接下来的任务就是实现异步逻辑,规范中是这样定义的:
onFulfilled
oronRejected
must 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
对应规范内容如下:
then
may 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
老样子,我们来看看规范中是如何描述的:
then
must return a promise [3.3].promise2 = promise1.then(onFulfilled, onRejected);
- If either
onFulfilled
oronRejected
returns a valuex
, run the Promise Resolution Procedure[[Resolve]](promise2, x)
.- If either
onFulfilled
oronRejected
throws an exceptione
,promise2
must be rejected withe
as the reason.- If
onFulfilled
is not a function andpromise1
is fulfilled,promise2
must be fulfilled with the same value aspromise1
.- If
onRejected
is not a function andpromise1
is rejected,promise2
must 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
promise
andx
refer to the same object, rejectpromise
with aTypeError
as 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
x
is a promise, adopt its state [3.4]:If
x
is pending,promise
must remain pending untilx
is fulfilled or rejected.If/when
x
is fulfilled, fulfillpromise
with the same value.If/when
x
is rejected, rejectpromise
with 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
x
is an object or functionLet
then
bex.then
.If retrieving the property
x.then
results in a thrown exceptione
, rejectpromise
withe
as 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
then
is a function, call it withx
asthis
, first argumentresolvePromise
, and second argumentrejectPromise
, where:
If/when
resolvePromise
is called with a valuey
, run[[Resolve]](promise, y)
.If/when
rejectPromise
is called with a reasonr
, rejectpromise
withr
.If both
resolvePromise
andrejectPromise
are 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
then
is not a function, fulfillpromise
withx
.If
x
is not an object or function, fulfillpromise
withx
.
修改代码如下:
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个测试用例全部通过,完美收官~