本文通过实现一个Promise对象,深入了解Promise的运行原理。 首先我们正式开始写代码,实现Promise實例前,思考一下当我们创建Promise實例时,JS到底做了什么。 举个例子:
let p = new Promise((resolve,reject) =>{
let a = 3 + 1;
a == 4 ? resolve(a) : reject(a);
})
我们创建一个Promise實例,里面需要放一个执行函数,而且函数必须要有resolve和reject的参数,用来保存成功的值或失败的原因以及修改状态。对象一创建,Promise的构造函数就运行传入的函数。上述例子,对象一创建,执行Promise实例存放的函数,然后调用resolve,把Promise的状态改为 fulfilled把成功的值存入,也就是4。好啦,现在我们可以开始写一个对象,叫MyPromise:
const PENDING = 'pending'; // 等待
const FULFILLED = 'fulfilled'; // 成功
const REJECTED = 'rejected'; // 失败
class MyPromise {
constructor(executor){
try {
executor(this.resolve, this.reject);
} catch(e) {
This.reject(e)
}
}
status = PENDING;
value = undefined;
reason = undefined;
resolve => value => {
if(this.status !== PENDING) return;
this.status = FULFILLED;
this.value = value}
reject => reason => {
if(this.status !== PENDING) return;
this.status = REJECTED;
this.reason = reason}
}
现在把一系列成员变量和常量定义好,也把构造函数写好了。其中,status是用来保存Promise的状态的,value用来保存成功的值,reason则是保存失败的原因,然后定义resolve和reject函数,它们分别改变status的值,而且判断如果status不为pending则退出,最后存入成功的值或失败的原因。
把构造函数和一系列成员变量写好后,现在我们来看Promise里最关键的方法 -- then。then里面有两个回调函数,根据状态而去调用,回调函数里保含Promise传来的值。例如:
let p = new Promise((resolve,reject) =>{
let a = 3 + 1;
a == 4 ? resolve(a) : reject(a);
})
p.then(res => console.log(res)) //結果是 4
then最后会传出一个新的Promise實例。现在我们来想想要怎么写。先不要去想具体内容,想一下大概形式是怎样。首先肯定要有两个参数作为成功和失败的回调函数,我们不妨叫它们作 successCallback, failCallback,然后最后结果一定是传出一个Promise實例。好,现在我们用代码来表示:
then (successCallback, failCallback) {
... //內容
let promise = new MyPromise((resolve, reject) => {
...
})
return promise
}
大概就是如上述般的写法。
接下来我们要思考到底then方法一开始是做什么的。它是要接收回调函数,如果放上非回调函数,则会直接在新的Promise中放入前一个Promise对象传入的值,即可理解为:
then(3) //等於 then(value => return Promise.resolve(value));
所以then会先判断传入的参数是否为回调函数,再作相应处理。写成代码形式:
then (successCallback, failCallback) {
successCallback = successCallback ? successCallback : value => value;
failCallback = failCallback ? failCallback: reason => { throw reason };
...
}
如果不是函数,则生成传入成功的值的函数或抛出失败原因的函数。接着就要生成一个新的Promise實例,执行函数根据前一个Promise的状态来决定调用的函数。这里先把成功与失败的处理写出来。
then (successCallback, failCallback) {
...
let promise = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
try {
let x = successCallback(this.value);
}catch (e) {
reject(e);
}
}else if (this.status === REJECTED) {
try {
let x = failCallback(this.reason);
}catch (e) {
reject(e);
}
}
});
return promise;
别忘了,then里面的回调函数是要放入微任务队列,因此我们需要调用 queueMicrotask,因此代码应写为:
...
let promise = new MyPromise((resolve, reject) => {
// 判断状态
if (this.status === FULFILLED) {
queueMicrotask(() => {
try {
let x = successCallback(this.value);
}catch (e) {
reject(e);
}
})
}else if (this.status === REJECTED) {
queueMicrotask(() => {
try {
let x = failCallback(this.reason);
}catch (e) {
reject(e);
}
})
} ...
return promise
好啦,现在then方法开始像样啦,但我们还需要去resolve或reject,这样这个Promise实例的状态才可确认下来。大可以把回调函数传回来的值直接resolve或reject,然而我们不知道传来的是什么,如果传来的是Promise自身,则会出现无穷循环,这意味着在resolve或reject之前,我们需要判断一下传来的值。我们可以写一个私有方法,在JS写私有方法,可以在class外面写一个函数 (如果是typescript,可以如面向对象语言,直接用私有方法)。
function resolvePromise (promise, x, resolve, reject) {
if (promise === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if (x instanceof MyPromise) {
// promise 对象
// x.then(value => resolve(value), reason => reject(reason));
x.then(resolve, reject);
} else {
// 普通值
resolve(x);
}}
先判断传来的值是否与所创建的 Promise 实例相同,如果相同,则报错。如果传来的是值,直接调用promise实例的resolve。若为 Promise实例,则直接调用该实例的then,把resolve和reject传入,作回调函数。注意,这里的resolve和reject是 promise 实例的,不是回调函数传出的Promise实例。在我们把这个私有方法放入then方法前,我们考虑一下最后一个状态--pending。
如果Promise的状态是pending,代码还没结束,所以还得运行下来,但由于状态还没有确定下来,只能先把then的回调函数存起来,等到执行函数运行完,确定状态,才把相应的回调函数提出来执行。因此我们需要重构一下Promise,增加两个数组,用来保存成功和失败的回调函数,然后resolve和reject确定相应数组有没有回调函数,如果有,则调用。相应的代码如下:
successCallback = [];
failCallback = [];
resolve = value => {
if (this.status !== PENDING) return;
this.status = FULFILLED;
this.value = value;
while(this.successCallback.length) this.successCallback.shift()() }
reject = reason => {
if (this.status !== PENDING) return;
this.status = REJECTED;
this.reason = reason;
while(this.failCallback.length) this.failCallback.shift()() }
新增两个数组,分别存入成功和失败的回调函数,resolve和reject通过先进先出的方式调出函数。
在then方法中,加上处理pending 相应的代码,调用存储函数的数组把回调函数保存下来:
this.successCallback.push(() => {
queueMicrotask(() => {
try {
let x = successCallback(this.value);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
});
this.failCallback.push(() => {
queueMicrotask(() => {
try {
let x = failCallback(this.reason);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
});
現在我們完成then方法了。完整代碼如下:
then (successCallback, failCallback) {
successCallback = successCallback ? successCallback : value => value;
failCallback = failCallback ? failCallback: reason => { throw reason };
let promise = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
queueMicrotask(() => {
try {
let x = successCallback(this.value);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
}else if (this.status === REJECTED) {
queueMicrotask(() => {
try {
let x = failCallback(this.reason);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
} else {
this.successCallback.push(() => {
queueMicrotask(() => {
try {
let x = successCallback(this.value);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
});
this.failCallback.push(() => {
queueMicrotask(() => {
try {
let x = failCallback(this.reason);
resolvePromise(promise, x, resolve, reject)
}catch (e) {
reject(e);
}
})
});
}
});
return promise; }
基本上,到了这里,Promise的实现完成大半,后面实现其馀的方法吧。
先说catch方法。catch负责调用失败回调函数,把之前传出的失败原因作为参数放进去。因此,代码可以这样写:
catch (failCallback) {
return this.then(undefined, failCallback) }
其实就是调用then,不过没有成功的回调函数。
resolve是一个静态方法,返回一个经resolve的Promise实例,即该Promise的状态是 fulfilled。
static resolve (value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value)); }}
根据传入的参数决定过执行过程。若为Promise实例,直接返回,否则,把参数放入一个新的Promise实例。
finally方法是不管成功或失敗,都必须调用传入的回调函数,返回一个新的Promise实例 。
finally (callback) {
return this.then(value => {
return MyPromise.resolve(callback()).then(() => value);
}, reason => {
return MyPromise.resolve(callback()).then(() => { throw reason })
}) }
all方法可以接受一个数组作为参数,传出一个新的Promise实例,里面有一个结果数组,把相应结果作为Promise的成功的值。如果数组里的元素为值,直接返回,作为结果。若为Promise,根据它的执行函数结果进行相应处理,若成功,放入结果数组,否则直接调用其失败函数,因此不会调用新的 Promise的resolve。
static all (array) {
let result = [];
let index = 0;
return new MyPromise((resolve, reject) => {
function addData (key, value) {
result[key] = value;
index++;
if (index === array.length) {
resolve(result);
}
}
for (let i = 0; i < array.length; i++) {
let current = array[i];
if (current instanceof MyPromise) {
// promise 对象
current.then(value => addData(i, value), reason => reject(reason))
}else {
// 普通值
addData(i, array[i]);
}
}
})
}
基本上Promise 的方法已经实现了,剩下的我放在后面附上的完整代码。
我们通过实现Promise,了解它的执行原理,而且在不知不觉间了解函数式编程。函数式编程,就是指用纯函数方式来编程。简单来说,纯函数就是指函数输入什么参数,输出结果不变,不管执行多少次,结果依然不变。Promise的实现也体现了该思想。当创建一个新的Promise实例,构造函数执行传入的函数,决定了Promise的状态,确定了成功的值和失败的值。构建后,实例的成员变量已经确定,之后不管调用任何方法,也不会改变里面的变量。
这样做有什么好处?好处就是保持不变性,调用一个实例的方法,其结果是预料的,方便测试。
最後附上完整代碼: