模拟实现Promise(上)
JavaScript 中的异步问题一直都让人苦恼,直到有了 Promise。Promise 是一种对未来值的封装,是一种许诺,类似于“我承诺将来会给你一个值”。用了这么久 Promise,对基本 API 的使用已经很熟练了,每次用的时候都感觉 Promise 就像魔法一般神奇。那么 Promise 是如何实现这种魔法的呢?
本篇文章将从头实现一个MyPromise
,它和 ES6 Promise 具有一样的 API。同时,为了书写方便,我们将借助 ES6 Class 来完成编码。
构造函数
首先我们知道 Promise 有三种状态,分别是pending
、fulfilled
和rejected
,并且状态改变只能由pending
向其他两种状态单向转移。同时,我们需要维护一个值用来存储 Promise 的决议值或者拒绝原因。Promise 的构造函数接收一个参数,名为executor
,它是一个函数,接收 Promise 传给它的resolve
和reject
两个函数作为参数。Promise 构造出来之后,我们可以调用 Promise 实例的.then
方法来注册决议或拒绝的事件监听函数,并且是可以注册多个的。结合上面这些信息,我们可以先搭一个框架出来。
// Promise的三种状态
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
// 工具函数,判断参数是否为函数
function isFunc(v) {
return typeof v === "function";
}
class MyPromise {
constructor(executor) {
// 所处状态
this._status = PENDING;
// 决议值,可能是完成值,也可能是拒绝原因
this._value = undefined;
// onFulfilled事件回调队列
this._fulfilledQueue = [];
// onRejected事件回调队列
this._rejectedQueue = [];
// 执行executor,如果发生错误
// 则将当前Promise设置为拒绝状态
try {
executor(this._resolve.bind(this), this._reject.bind(this));
} catch (err) {
this._reject(err);
}
}
this._resolve() {}
this._reject() {}
}
首先把三种状态抽象成变量方便以后使用,然后我们声明了一个工具函数isFunc
,用来判断参数是否是函数。在构造函数内,除了存储状态的变量和存储决议值或拒绝原因的变量外,还需要声明两个队列,用来存储 Promise 决议或拒绝前,被注册的事件回调。目前this._reject
和this._resolve
还没有被实现,不要急,接着往下看。
_reject
_reject
执行时,Promise 应处于pending
状态,否则说明 Promise 已经决议或拒绝。如果状态没问题的话,则设置当前 Promise 的状态为rejected
,同时把拒绝原因存储起来,然后从拒绝事件回调队列中取出所有回调一一执行。我们来按照这个思路实现一下。
class MyPromise {
// ...
_reject(reason) {
// 保证Promise的状态转换只能发生一次
if (this._status !== PENDING) return;
const run = () => {
this._value = reason;
this._status = REJECTED;
// 从相应队列里取出所有回调函数并执行
let cb;
while ((cb = this._rejectedQueue.shift())) {
cb(reason);
}
};
// 为了支持"同步"的Promise
// 这里使用setTimeout来异步执行run
setTimeout(run, 0);
}
}
注意到我们把执行回调函数的过程封装在了setTimeout
中,这是为什么呢?直接同步执行这个逻辑可不可以呢?考虑下面的代码
var p = new Promise((resolve, reject) => {
reject();
});
p.then(null, () => {
console.log("p is rejected");
});
Promise 并不止被用于异步过程,它的构造函数完全可以不包含任何异步过程。在上面的代码中,我们构造 Promise 的同时直接将其同步拒绝了,而且此时还没有执行到p.then
这里,也就是说还没有注册上拒绝事件回调。因此从事件回调队列中取回调函数并一一执行的过程需要是异步的,这样才能保证像上面这段代码一样的“同步”Promise 能够正常执行。
_resolve
与_reject
类似,_reject
首先要保证状态转义符合要求,同时取出回调事件执行。但比_reject
更强大的是,如果传参(也就是决议值)是 Promise 实例的话,_resolve
则会跟踪自己的传参,因此决议值会变成传参的决议值而不是传参本身。举个例子
var p = new Promise((resolve, reject) => {
resolve(Promise.resolve(42));
});
p.then(value => {
console.log("p is resolved with", value);
});
// -> p is resolved with 42
接下来我们来看一下如何实现这个神奇的特性
class MyPromise {
// ...
_resolve(value) {
if (this._status !== PENDING) return;
const run = () => {
const runFulfilled = value => {
let cb;
while ((cb = this._fulfilledQueue.shift())) {
cb(value);
}
};
const runRejected = reason => {
let cb;
while ((cb = this._rejectedQueue.shift())) {
cb(reason);
}
};
if (value instanceof MyPromise) {
value.then(
v => {
this._value = v;
this._status = FULFILLED;
runFulfilled(v);
},
r => {
this._value = r;
this._status = REJECTED;
runRejected(r);
}
);
} else {
this._value = value;
this._status = FULFILLED;
runFulfilled(value);
}
};
setTimeout(run, 0);
}
}
整体结构上和_reject
类似,但是_resolve
中多了一个用来判断传参类型的逻辑,即传参如果是 Promise 实例,则在传参上注册.then
事件,然后根据传参最终被决议还是拒绝来决定当前 Promise 应该被决议还是拒绝。最后,依然是由于“同步”Promise 的问题,需要把整个过程用setTimeout
包裹起来,确保逻辑执行时已经被注册了事件回调。
then
根据规范,then
位于 Promise 的实例上,每次调用都会返回一个新的 Promise 实例。它的作用是注册事件回调,决议事件回调一般被称为onFulfilled
,拒绝事件回调一般被称为onRejected
。then
也有一个神奇的特性,如果onFulfilled
或者onRejected
返回了 Promise 实例,则then
返回的新 Promise 实例会跟踪这些返回值 Promise。如下面的例子所示。
var p = new Promise((resolve, reject) => {
resolve(Promise.resolve());
});
var p1 = p.then(() => {
return Promise.resolve(42);
});
p1.then(value => {
console.log("p1 is resolved with", value);
});
// -> p1 is resolved with 42
具体实现如下
class MyPromise {
// ...
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
function fulfilled(value) {
try {
if (!isFunc(onFulfilled)) {
resolve(value);
} else {
let ret = onFulfilled(value);
if (ret instanceof MyPromise) {
ret.then(resolve, reject);
} else {
resolve(ret);
}
}
} catch (err) {
reject(err);
}
}
function rejected(reason) {
try {
if (!isFunc(onRejected)) {
reject(reason);
} else {
let ret = onRejected(reason);
if (ret instanceof MyPromise) {
ret.then(resolve, reject);
} else {
reject(ret);
}
}
} catch (err) {
reject(err);
}
}
switch (this._status) {
case PENDING:
this._fulfilledQueue.push(onFulfilled);
this._rejectedQueue.push(onRejected);
break;
case FULFILLED:
fulfilled(this._value);
break;
case REJECTED:
rejected(this._value);
break;
}
});
}
}
可以看到总体结构是这样的,首先无脑构造一个 Promise 实例并返回,在该 Promise 实例的构造函数中,首先判断一下当前 Promise 的状态,如果还没被决议或拒绝,则先将事件回调存储在队列中。否则的话,直接调用相应的事件回调函数,判断返回值是否是 Promise 实例,如果是的话则追踪,不是的话新 Promise 将用该返回值决议或拒绝。
其中用到了我们之前声明的isFunc
工具函数,因为在 MDN 中有这样一段描述
注意:如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么 then 方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用 then 的 Promise 的状态(fulfillment 或 rejection)发生改变,但是 then 中并没有关于这种状态的回调函数,那么 then 将创建一个没有经过回调函数处理的新 Promise 对象,这个新 Promise 只是简单地接受调用这个 then 的原 Promise 的终态作为它的终态。
什么意思呢,我们还是举个例子来说明吧
var p = new Promise(resolve => {
resolve(42);
});
p.then(null, () => {
console.log("I will never run");
}).then(value => {
console.log("p.then() is resolved with", value);
});
// -> p.then() is resolved with 42
相应回调函数缺失或者不是函数时,效果就像决议值或拒绝原因被“透传”给.then
返回的 Promise 的相应回调函数了。
总结
至此,我们就算模拟实现了一个比较完善的基础 Promise。实现过程中,我们可以发现看似神奇的 Promise 背后其实并没有魔法,它本质上还是基于回调实现的。目前我们实现的这个 Promise 只具有一些基本功能,而剩下的一些 API,如Promise.prototype.catch
、Promise.prototype.finally
、Promise.all
等,留到下一篇再详细介绍吧~