尝试使用 js
手写 Promise
类,实现部分功能,遵守 Promises/A+
规范,例如 then, catch, all, race, resolve, reject
等。
使用原生 Promise
查看效果
let promise1 = new Promise((resolve, reject) => {
resolve(1);
});
promise1
.then(res => {
console.log(res);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 1000);
});
})
.then(res => {
console.log(res);
throw new Error();
})
.catch(err => {
console.log(err);
});
// 1
// 1秒后
// 2
// Error
从上述代码中可以看出 promise
的几个性质,首先它是一个类,可以被实例化,参数为一个函数,有 resolve
和 reject
两个参数。当期望的事情完成后,调用 resolve
表明成功,调用 reject
表明失败。成功后可以调用 .then
来进行后续操作,默认参数为 resolve
函数的参数。返回值可以是一个新的 promise
,后续操作会等待这个 promise
的完成。抛出错误时或是手动调用 reject
都会进入 catch
回调,或是 .then
方法的第二个参数。
接下来,我们一步步实现。
实现过程
定义
promise
构造函数首先定义三个常量用于表示状态,
Promises/A+
规范中提到:A promise must be in one of three states: pending, fulfilled, or rejected.
When pending, a promise:
- may transition to either the fulfilled or rejected state.
When fulfilled, a promise:
- must not transition to any other state.
must have a value, which must not change.
When rejected, a promise:
- must not transition to any other state.
must have a reason, which must not change.
Here, “must not change” means immutable identity (i.e.
===
), but does not imply deep immutability.promise
仅有三种状态,表示还未完成,已成功或是已失败。当处于pending
状态时,可能会转换为另外两种状态。当处于fulfilled
状态时,不会再转为其余状态,必须拥有一个value
属性且不再变化。当处于rejected
状态时,同样不会再转变,且必须拥有一个reason
属性不再变化。const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = () => {};
const reject = () => {};
executor(resolve, reject);
}
}
所以在构造函数中,我们定义三个变量,初始化此时的状态为 `pending` ,并且传入的 `executor` 函数我们需要进行调用,传入定义的两个函数作为参数。
然后定义 `resolve` 和 `reject` 的内容。
```js
const resolve = value => {
if (this.status !== PENDING) return;
this.status === FULFILLED;
this.value = value;
};
const reject = reason => {
if (this.status !== PENDING) return;
this.status === REJECTED;
this.reason = reason;
};
遵循规范,若当前状态不是 pending
,不执行后续操作。
定义
then
方法首先查看规范:
A promise’s
then
method accepts two arguments:promise.then(onFulfilled, onRejected)
Both
onFulfilled
andonRejected
are optional arguments:- If
onFulfilled
is not a function, it must be ignored. - If
onRejected
is not a function, it must be ignored.
- If
If
onFulfilled
is a function:- it must be called after
promise
is fulfilled, withpromise
’s value as its first argument. - it must not be called before
promise
is fulfilled. - it must not be called more than once.
- it must be called after
If
onRejected
is a function,- it must be called after
promise
is rejected, withpromise
’s reason as its first argument. - it must not be called before
promise
is rejected. - it must not be called more than once.
- it must be called after
onFulfilled
oronRejected
must not be called until the execution context stack contains only platform code. [3.1].onFulfilled
andonRejected
must be called as functions (i.e. with nothis
value). [3.2]then
may be called multiple times on the same promise.- If/when
promise
is fulfilled, all respectiveonFulfilled
callbacks must execute in the order of their originating calls tothen
. - If/when
promise
is rejected, all respectiveonRejected
callbacks must execute in the order of their originating calls tothen
.
- If/when
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
.
- If either
规范提到,
then
方法的两个参数是可选的,但必须是函数,不是函数的话将被忽略。onFulfilled
将在promise
状态为fulfilled
之后调用,promise
的value
作为它的参数,在promise
转为fulfilled
之前不允许调用,且只能被调用一次。onRejected
类似。两个函数都只能在执行上下文调用栈仅包含
platform code
时调用,需要确保两个函数都是异步调用的。两个函数都必须以函数形式调用,不允许包含
this
。同一个
promise
可以多次调用then
方法(此处不是指链式调用),需要确保多次调用是按照顺序执行的。then
方法的返回值也是一个promise
。onFulfilled
或是onRejected
的返回值需要执行Promise Resolution Procedure
。两个函数若是抛出了错误,
promise2
需要以该错误为参数被拒绝。若没有传入两个函数,
promise
值需要同样往后续传递。then(onFulfilled, onRejected) { if (this.status === FULFILLED) { onFulfilled(this.value); } if (this.status === REJECTED) { onRejected(this.reason); } }
首先简单实现同步回调,判断当前的
status
来执行相应的回调。接下来考虑异步执行,若是
executor
内部是一个异步执行,异步执行中才会调用resolve
或是reject
,那么我们在调用then
时,status
还没有得到转换,此时无法正确执行,且需要在异步地更改status
后再执行回调。修改代码:
// ... class MyPromise { constructor(executor) { // ... this.onFulfilledCallList = []; this.onRejectedCallList = []; const resolve = value => { // ... this.onFulfilledCallList.forEach(fn => fn()); }; const reject = reason => { // ... this.onRejectedCallList.forEach(fn => fn()); }; // ... } then(onFulfilled, onRejected) { // ... if (this.status === PENDING) { this.onFulfilledCallList.push(() => { onFulfilled(this.value); }); this.onRejectedCallList.push(() => { onRejected(this.reason); }); } } }
此时的异步
resolve
已经可以正确执行。再来考虑错误捕获的问题,我们的
executor
中可能会抛出错误,此时需要直接执行reject
来拒绝。修改代码:
// ... class MyPromise { constructor(executor) { // ... try { executor(resolve, reject); } catch (e) { reject(e); } } // ... }
在执行
executor
时就尝试捕获错误,若是捕获到了就直接reject
,此时需要定义then
的第二个参数来处理。此时若是对
promise
实例多次调用then
方法也可以正确且按序执行。接下来处理两个参数为空的情况。
// ... class MyPromise { // ... then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; }; // ... } }
接下来处理
then
的返回值问题,即实现链式调用。参考原生
promise
的表现形式:let promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); promise1 .then( res => { console.log('10', res); // 10 1 return '11'; } ) .then( res => { console.log('16', res); // 16 11 return new Promise((resolve, reject) => { resolve('18'); }); } ) .then( res => { console.log('24', res); // 24 18 } );
可以看出,每个
then
方法的参数都是前一次then
的返回值,返回值若是promise
,则是resolve
的值。于是我们需要处理两种状况,
return
了原始值或是promise
。于是我们对
onFulfilled
和onRejected
返回值进行一个处理。在那之前,我们先注意若是
onFulfilled
和onRejected
函数内部执行时抛出了错误,我们需要进行reject
。所以代码修改如下:// ... function promiseResolve(promise2, x, resolve, reject) { console.log(promise2, x, resolve, reject); } class MyPromise { // ... then(onFulfilled, onRejected) { // ... let promise2 = new MyPromise((resolve, reject) => { if (this.status === FULFILLED) { setTimeout(() => { try { let x = onFulfilled(this.value); promiseResolve(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === REJECTED) { setTimeout(() => { try { let x = onRejected(this.reason); promiseResolve(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === PENDING) { this.onFulfilledCallList.push(() => { try { let x = onFulfilled(this.value); promiseResolve(promise2, x, resolve, reject); } catch (e) { reject(e); } }); this.onRejectedCallList.push(() => { try { let x = onRejected(this.reason); promiseResolve(promise2, x, resolve, reject); } catch (e) { reject(e); } }); } }); return promise2; } }
此处做了较多修改,在
then
方法中定义了一个新的promise
实例,然后进行状态判定,若是已完成,则进行相应操作。若是未完成,则把回调进行订阅,在完成时进行发布。调用回调时需要得到返回值,然后进行返回值的处理。于是定义了一个resolvePromise
方法来处理,我们会在这个函数内部进行resolve
或是reject
,所以需要传入对应函数。同时传入promise
本身和x
这个返回值来自于规范内的规定。但是,在调用
promiseResolve
时,我们的promise2
还没有执行完毕,所以我们需要使用异步方法setTimeout
来延迟这个函数的执行,确保可以正确得到promise2
。同时,我们也要对回调函数的执行进行错误捕获,若是捕获到错误就reject
。为什么订阅回调时不需要异步来调用处理函数呢?因为订阅的回调发布时已经是异步状态。
然后开始处理
promiseResolve
函数。首先看规范:
The promise resolution procedure is an abstract operation taking as input a promise and a value, which we denote as
[[Resolve]](promise, x)
. Ifx
is a thenable, it attempts to makepromise
adopt the state ofx
, under the assumption thatx
behaves at least somewhat like a promise. Otherwise, it fulfillspromise
with the valuex
.This treatment of thenables allows promise implementations to interoperate, as long as they expose a Promises/A+-compliant
then
method. It also allows Promises/A+ implementations to “assimilate” nonconformant implementations with reasonablethen
methods.To run
[[Resolve]](promise, x)
, perform the following steps:If
promise
andx
refer to the same object, rejectpromise
with aTypeError
as the reason.If
x
is a promise, adopt its state :- 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.
- If
Otherwise, if
x
is an object or function,Let
then
bex.then
. [3.5]If retrieving the property
x.then
results in a thrown exceptione
, rejectpromise
withe
as the reason.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.If calling
then
throws an exceptione
,If
resolvePromise
orrejectPromise
have been called, ignore it.Otherwise, reject
promise
withe
as the reason.
If
then
is not a function, fulfillpromise
withx
.
If
x
is not an object or function, fulfillpromise
withx
.
If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of
[[Resolve]](promise, thenable)
eventually causes[[Resolve]](promise, thenable)
to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and rejectpromise
with an informativeTypeError
as the reason. [3.6]简单翻译一下:
此处使用
promise2
指代第一个参数。如果
promise2
和x
引用的是同一个对象,直接以类型错误reject
。如果
x
是一个promise
:- 如果
x
的状态是pending
,那么promise2
需要保持直到x
转为fulfilled
或者是rejected
。 - 如果
x
的状态是fulfilled
,使用x
的value
去转换promise2
为fulfilled
。 reject
同上。
- 如果
如果
x
是一个对象或函数:- 将
x.then
赋值给then
。 - 若是
x.then
的结果抛出了错误,使用这个错误来reject
promise2
。 - 如果
then
是一个函数就调用它,第一个参数为resolvePromise
,第二个参数为rejectPromise
。resolvePromise
接收一个参数y
,它的类型也可能是promise
,所以调用promiseResolve
来处理它。rejectPromise
接收一个参数r
,使用它来reject
promise2
。- 如果两个回调参数都被调用,或是多次使用相同参数重复调用,只有第一次调用生效,其余的会被忽略。
- 如果调用
x.then
抛出错误:- 如果回调参数已经被调用了,忽略。
- 其余情况,使用错误来
reject
promise2
。
- 如果
then
不是一个函数,使用x
来fulfilled
promise2
。
- 将
如果
x
是其他类型,使用x
来fulfilled
promise2
。
// ... function promiseResolve(promise2, x, resolve, reject) { if (promise2 === x) { return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } let called = false; if ((typeof x === 'object' && x !== null) || typeof x === 'function') { try { let then = x.then; if (typeof then === 'function') { then.call( x, y => { if (called) return; called = true; promiseResolve(promise2, y, resolve, reject); }, r => { if (called) return; called = true; reject(r); } ); } else { resolve(x); } } catch (err) { if (called) return; called = true; reject(err); } } else { resolve(x); } } class MyPromise { // ... }
此处已经可以正确处理链式调用,返回新的
promise
实例的情况。且嵌套返回也没有问题。let promise1 = new MyPromise((resolve, reject) => { resolve(1); }); let promise2 = promise1.then( res => { return res; }, err => { return err; } ); promise2 .then() .then() .then() .then( res => { console.log('19', res); // 19 1 return new MyPromise((resolve, reject) => { resolve( new MyPromise((resolve, reject) => { resolve('23'); }) ); }); }, err => { console.log(err); } ) .then( res => { console.log('34', res); // 34 19 return new MyPromise((resolve, reject) => { resolve('36'); }); }, err => { console.log('40', err); } ) .then( res => { console.log('45', res); // 45 34 }, err => { console.log('48', err); } );
接下来实现
catch
方法:// ... class MyPromise { // ... catch(onRejected) { return this.then(null, onRejected); } }
如代码所示,
catch
的逻辑其实与then
类似,只是没有传入onFulfilled
回调而已。接下来实现
Promise.resolve
Promise.reject
方法:// ... class MyPromise { // ... static resolve(onResolved) { return new MyPromise((resolve, reject) => { if (onResolved instanceof MyPromise) { onResolved.then(resolve, reject); } else { resolve(onResolved); } }); } static reject(onRejected) { return new MyPromise((resolve, reject) => { reject(onRejected); }); } }
两个函数都返回一个
promise
即可。resolve
内部需要进行判断,如果传入的值为一个promise
,那么我们就进行处理。接下来实现
Promise.all
方法:// ... class MyPromise { // ... static all(promises) { let len = promises.length; let values = []; let resolvedCount = 0; return new MyPromise((resolve, reject) => { promises.forEach(p => { MyPromise.resolve(p).then( res => { values.push(res); resolvedCount++; if (resolvedCount === len) { resolve(values); } }, err => { reject(err); } ); }); }); } }
all
方法等待传入的参数全部执行完成,返回所有返回值的数组。我们遍历参数列表,对每一个参数进行
resolve
。将结果加入value
数组中,并计数。当返回值数量等于参数数量时,可以resolve
返回值。最后实现
race
方法,这个方法只需要resolve
最先得到结果的值即可。// ... class MyPromise { // ... static race(promises) { return new MyPromise((resolve, reject) => { promises.forEach(p => { MyPromise.resolve(p).then( res => { resolve(res); }, err => { reject(err); } ); }); }); } }
总结
写完以后看了一下时间,完成时间是 15:58
。花了两个半小时,中间也有一些地方写乱了,然后又去查资料。感觉对 promise
理解加深了一些,但又好像还是很混乱。
总的来说, promise
改善了回调地狱的问题,我们可以在回调中将结果抛出,然后在后续链式调用获取结果。