模拟实现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等,留到下一篇再详细介绍吧~