前言
如果我们想要深入地学习 JavaScript,那么手写 Promise 无可避免。
手写 Promise 需要使用多种技术手段和逻辑思路,对学习 JavaScript 或者其他语言、框架等都可以起到积极的作用。
本篇文章详细记录笔者尝试实现 Promise 的每个步骤以及在此过程中遇到的各种问题。
Promises/A+ 规范
Promise 并不是一个新事物,而是按照一个规范实现的类。这个规范有多个版本,如 Promises/A、Promises/B、Promises/D 以及 Promises/A+ 等。ES6 采用 Promises/A+ 这一版,那么我们自然也就按照这一版来实现 Promise。
Promises/A+ 规范包含术语(Terminology)、要求(Requirements)和注意事项(Notes)3 个部分。我们主要按第二部分逐步实现 Promise。
实现步骤
I. Promise States
一个合乎规范的 promise 应该有 pending
、fulfilled
、rejected
三个状态;
promise 的状态可以由 pending 转为 fulfilled
或者 rejected
,这个转换过程不可逆;
fulfilled
和 rejected
状态不可再转变为其他状态;
当 promise 状态由 pending
转变为 fulfilled
时,必须有一个 value
且不可改变;
当 promise 状态由 pending
转变为 rejected
时,必须有一个 reason
且不可改变;
value
和 reason
的 “不可改变”是指 指向不变,非 deep 层级的不可改变。
Here, “must not change” means immutable identity (i.e. ===
), but does not imply deep immutability.
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| // 3个状态 const stateEnum = { PENDING: 'PENDING', FULFILLED: 'FULFILLED', REJECTED: 'REJECTED', };
class MyPromise { constructor(executor) { this.state = stateEnum.PENDING; // 当前状态,初始为 pending this.value = null; // 状态转变为 fulfilled 时的 value this.reason = null; // 状态转变为 rejected 时的 reason
// 更改状态;因 resolve/reject 在 executor 中执行,使用箭头函数固定 this 指向 const resolve = (value) => { // pending => fulfilled 单向不可逆 if (this.state === stateEnum.PENDING) { this.state = stateEnum.FULFILLED; this.value = value; } }; const reject = (reason) => { // pending => rejected 单向不可逆 if (this.state === stateEnum.PENDING) { this.state = stateEnum.REJECTED; this.reason = reason; } };
// 立即执行 try { // 传入方法 resolve, reject executor(resolve, reject); } catch (err) { // 执行异常时,失败回调 reject(err); } } }
const promise = new MyPromise();
|
II. The then
Method
一个 promise 应该提供一个 then
方法,用于获取这个 promise 当前/最终的 value 或 reason;
then
方法接收两个可选参数 onFulfilled
和 onRejected
:
1
| promise.then(onFulfilled, onRejected)
|
参数 onFulfilled
和 onRejected
均为可执行函数;
promise 在 fulfilled
之后调用函数 onFulfilled
,参数为 promise 的 value
;
promise 在 rejected
之后调用函数 onRejected
,参数为 promise 的 reason
;
1 2 3 4 5 6 7 8 9 10 11 12
| ... class MyPromise { ... then(onFulFilled, onRejected) { // onFulFilled / onRejected 必须是函数 typeof onFulFilled === 'function' ? onFulFilled : (v) => v; typeof onRejected === 'function' ? onRejected : (err) => { throw err };
if (this.state === stateEnum.FULFILLED) onFulFilled(this.value); if (this.state === stateEnum.REJECTED) onRejected(this.reason); } }
|
只有当执行栈仅包含平台代码时,onFulfilled
和 onRejected
才可以执行 ;
- 用以确保
onFulfilled
和 onRejected
可以异步地在新的执行栈中执行;
- 可以通过宏任务(如
setTimeout
)、微任务(如 process.nextTick
)来实现;
then
可以被同一个 promise 调用多次;
- 当 promise 状态发生改变时,
onFulfilled
和 onRejected
必须按其注册顺序依次执行;
then
函数必须返回一个 promise;
1
| promise2 = promise1.then(onFulfilled, onRejected);
|
- 如果
onFulfilled
或 onRejected
返回一个值(假定为 x
),调用 Promise 解析程序([[Resolve]](promise2, x)
);
- 如果
onFulfilled
或 onRejected
抛出一个异常(假定为 e
),promise2 必须以 e
为 reason
值转变为 rejected
状态;
- 如果
onFulfilled
不是函数,且 promise1 已经处于 fulfilled
状态,promise2 必须以和 promise1 相同的 value
转变为 fulfilled
状态;
- 如果
onRejected
不是函数,且 promise1 已经处于 rejected
状态,promise2 必须以和 promise1 相同的 reason
转变为 rejected
状态;
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| // 3个状态 const stateEnum = { PENDING: 'PENDING', FULFILLED: 'FULFILLED', REJECTED: 'REJECTED', };
class MyPromise { // 构造函数,参数为可执行函数 constructor(executor) { this.state = stateEnum.PENDING; // 当前状态,初始为 pending this.value = null; // 状态转变为 fulfilled 时的 value this.reason = null; // 状态转变为 rejected 时的 reason
// 更改状态;因 _resolve/_reject 在 executor 中执行,使用箭头函数固定 this 指向 const _resolve = (value) => { // pending => fulfilled 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.FULFILLED; this.value = value; } }; const _reject = (reason) => { // pending => rejected 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.REJECTED; this.reason = reason; } };
try { // 立即执行 executor 并传入实参 _resolve, _reject executor(_resolve, _reject); } catch (err) { // 执行异常时,失败回调 _reject(err); } }
then(onFulFilled, onRejected) { // onFulFilled / onRejected 必须是函数 typeof onFulFilled === 'function' ? onFulFilled : (v) => v; typeof onRejected === 'function' ? onRejected : (err) => { throw err };
if (this.state === stateEnum.FULFILLED) onFulFilled(this.value); if (this.state === stateEnum.REJECTED) onRejected(this.reason); } }
const promise = new MyPromise();
|
至此,我们已经实现了一个简单的 Promise 。简单验证如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const result = 'resolve'; const promise = new MyPromise((resolve, reject) => { if (result === 'fulfilled') { resolve('fulfilled'); } else { reject('rejected'); } }); promise.then( (res) => { console.log('fulfilled value:', res); }, (err) => { console.log('rejected reason:', err); } );
|
程序正常,没有问题。
但是,参考 Promises/A+ 标准和正常 Promise 的使用方式,这个 Promise 还是存在许多问题:
上述 6、7、8 条标准未实现;
由于 executor 中可能存在异步操作,以至于 then 执行时,promise 可能仍然处于 pending
状态,也就是说,onFulfilled
和 onRejected
的执行时机有问题;
按上述规则 No.6(Promises/A+ 2.2.4)的要求,onFulfilled
和 onRejected
需要异步调用,这里是同步。
Promises/A+ 3.1 “In practice, this requirement ensures that onFulfilled
and onRejected
execute asynchronously”
我们姑且先解决问题 2 和 3,规则 No.6 和 No.7。至于规则 No.8,等我们阅读完 III. The Promise Resolution Procedure 这一部分再来解决。
A. 解决异步 executor
的问题
来捋一捋思路:
- 在我们
promise.then()
时,then
就已经执行,我们能做的就是将延迟执行 then
的两个函数参数 onFulfilled
和 onRejected
,而不是延迟 then
;
resolve
和 reject
在 executor
内部执行,按正常的 Promise 的用法,就是在异步函数的最后执行。那么,不妨将 onFulfilled
和 onRejected
放到 resolve
/ reject
中执行以达到延迟执行的目的;
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| class MyPromise { // 构造函数,参数为可执行函数 constructor(executor) { this.state = stateEnum.PENDING; // 当前状态,初始为 pending this.value = null; // 状态转变为 fulfilled 时的 value this.reason = null; // 状态转变为 rejected 时的 reason this.onFulfilled = v => v; // 存储,以延迟执行 this.onRejected = v => v; // 存储,以延迟执行
// 更改状态;因 _resolve/_reject 在 executor 中执行,使用箭头函数固定 this 指向 const _resolve = (value) => { // pending => fulfilled 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.FULFILLED; this.value = value; // 状态改变时执行 this.onFulfilled(); } }; const _reject = (reason) => { // pending => rejected 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.REJECTED; this.reason = reason; // 状态改变时执行 this.onRejected(); } };
try { // 立即执行 executor 并传入实参 _resolve, _reject executor(_resolve, _reject); } catch (err) { // 执行异常时,失败回调 _reject(err); } }
then(onFulfilled, onRejected) { // onFulfilled / onRejected 必须是函数 typeof onFulfilled === 'function' ? onFulfilled : (v) => v; typeof onRejected === 'function' ? onRejected : (err) => { throw err };
switch (this.state) { case stateEnum.FULFILLED: onFulfilled(this.value); break; case stateEnum.REJECTED: onRejected(this.reason); break; case stateEnum.PENDING: // 当状态为 pending 时,存储而不执行 // 使用箭头函数嵌套,确定 this 指向 this.onFulfilled = () => { onFulfilled(this.value); } this.onRejected = () => { onRejected(this.value); } break; } } }
|
我们给 MyPromise 类新增了两个属性 onFulfilled
和 onFulfilled
,在 then
执行时,若 promise
的状态仍为 pending
,那么把 then
的两个参数存储起来(赋值给 this.onFulfilled
和 this.onRejected
),在状态变化(_resolve
或 _reject
执行时)时调用。如此,便可以实现延迟调用、异步执行了。
B. 规则 No.6:onFulfilled
和 onRejected
异步调用
根据 Promises/A+ 规则 3.1 的描述,我们使用 setTimeout 来保证 onFulfilled
和 onRejected
的异步调用。
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class MyPromise { ... then(onFulfilled, onRejected) { ... switch (this.state) { ... case stateEnum.PENDING: this.onFulfilled = () => { // setTimeout 实现异步执行 setTimeout(() => { onFulFilled(this.value); }, 0); } this.onRejected = () => { // setTimeout 实现异步执行 setTimeout(() => { onFulFilled(this.value); }, 0); } break; } } }
|
C. 规则 No.7:then
的多次调用
按上述规则 No.7(Promises/A+ 2.2.6),同一个 promise 可以多次调用 then,也就是说会有这种情况:
1 2 3 4 5 6 7 8
| promise.then( res => { console.log('res-1') }, err => { console.log('err-1') } ); promise.then( res => { console.log('res-2') }, err => { console.log('err-2') } );
|
我们目前实现的 Promise ,由于 this.onFulfilled
和 this.onRejected
都是直接赋值的,就导致上面这种用法会有覆盖的风险。
既然直接赋值的方式不行,不妨使用数组存储。需要时取出执行,之后去除数组元素即可。
优化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| class MyPromise { // 构造函数,参数为可执行函数 constructor(executor) { this.state = stateEnum.PENDING; // 当前状态,初始为 pending this.value = null; // 状态转变为 fulfilled 时的 value this.reason = null; // 状态转变为 rejected 时的 reason this.onFulFilledQueue = []; // fulfilled 时调用的函数执行队列,存储 then 的 onFulFilled 函数 this.onRejectedQueue = []; // rejected 时调用的函数执行队列,存储 then 的 onRejected 函数
// 更改状态;因 _resolve/_reject 在 executor 中执行,使用箭头函数固定 this 指向 const _resolve = (value) => { // pending => fulfilled 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.FULFILLED; this.value = value; // 状态改变时执行 while (this.onFulFilledQueue.length) { const cb = this.onFulFilledQueue.shift(); cb && cb(); } } }; const _reject = (reason) => { // pending => rejected 单向不可逆 if (this.state === stateEnum.PENDING) { // 状态变化 this.state = stateEnum.REJECTED; this.reason = reason; // 状态改变时执行 while (this.onRejectedQueue.length) { const cb = this.onRejectedQueue.shift(); cb && cb(); } } }; ... }
then(onFulFilled, onRejected) { ... switch (this.state) { ... case stateEnum.PENDING: // 当状态为 pending 时,存储而不执行 // 使用箭头函数嵌套,确定 this 指向 this.onFulFilledQueue.push(() => { // setTimeout 实现异步执行 setTimeout(() => { onFulFilled(this.value); }, 0); }); this.onRejectedQueue.push(() => { // setTimeout 实现异步执行 setTimeout(() => { onRejected(this.reason); }); }); break; } } }
|
很明显,我们采用了发布订阅模式来实现延迟执行。
III. The Promise Resolution Procedure
The Promise Resolution Procedure
,姑且称之为 Promise 解析程序,函数表示为 [[Resolve]](promise, x)
代码实现如下
IV. then 链式调用与值穿透
当 then 函数 return 了一个值,我们总能在下一个 then(onFulfilled)中取到,这就是 then 的链式调用;
而如果前一个 then 函数没有传入任何参数(promise.then().then()
),则可以在后面的 then 中获取之前 then 返回的值,此即 then 的值穿透。
小结
Promises/A+
Promise 知识汇总和面试情况
面试官:“你能手写一个 Promise 吗”