前言

如果我们想要深入地学习 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

  1. 一个合乎规范的 promise 应该有 pendingfulfilledrejected 三个状态;

  2. promise 的状态可以由 pending 转为 fulfilled 或者 rejected,这个转换过程不可逆;

  3. fulfilledrejected 状态不可再转变为其他状态;

  4. 当 promise 状态由 pending 转变为 fulfilled 时,必须有一个 value 且不可改变;

  5. 当 promise 状态由 pending 转变为 rejected 时,必须有一个 reason 且不可改变;

  6. valuereason 的 “不可改变”是指 指向不变,非 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

  1. 一个 promise 应该提供一个 then 方法,用于获取这个 promise 当前/最终的 value 或 reason;

  2. then 方法接收两个可选参数 onFulfilledonRejected

    1
    promise.then(onFulfilled, onRejected)
  3. 参数 onFulfilledonRejected 均为可执行函数;

  4. promise 在 fulfilled 之后调用函数 onFulfilled ,参数为 promise 的 value

  5. 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);
    }
    }
  6. 只有当执行栈仅包含平台代码时,onFulfilledonRejected 才可以执行 ;

    1. 用以确保 onFulfilledonRejected 可以异步地在新的执行栈中执行;
    2. 可以通过宏任务(如 setTimeout)、微任务(如 process.nextTick)来实现;
  7. then 可以被同一个 promise 调用多次;

    1. 当 promise 状态发生改变时,onFulfilledonRejected 必须按其注册顺序依次执行;
  8. then 函数必须返回一个 promise;

    1
    promise2 = promise1.then(onFulfilled, onRejected);
    1. 如果 onFulfilledonRejected 返回一个值(假定为 x ),调用 Promise 解析程序([[Resolve]](promise2, x));
    2. 如果 onFulfilledonRejected 抛出一个异常(假定为 e ),promise2 必须以 ereason 值转变为 rejected 状态;
    3. 如果 onFulfilled 不是函数,且 promise1 已经处于 fulfilled 状态,promise2 必须以和 promise1 相同的 value 转变为 fulfilled 状态;
    4. 如果 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 还是存在许多问题:

  1. 上述 6、7、8 条标准未实现;

  2. 由于 executor 中可能存在异步操作,以至于 then 执行时,promise 可能仍然处于 pending 状态,也就是说,onFulfilledonRejected 的执行时机有问题;

  3. 按上述规则 No.6(Promises/A+ 2.2.4)的要求,onFulfilledonRejected 需要异步调用,这里是同步。

    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 的问题

来捋一捋思路:

  1. 在我们 promise.then() 时,then 就已经执行,我们能做的就是将延迟执行 then 的两个函数参数 onFulfilledonRejected ,而不是延迟 then
  2. resolverejectexecutor 内部执行,按正常的 Promise 的用法,就是在异步函数的最后执行。那么,不妨将 onFulfilledonRejected 放到 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 类新增了两个属性 onFulfilledonFulfilled,在 then 执行时,若 promise 的状态仍为 pending,那么把 then 的两个参数存储起来(赋值给 this.onFulfilledthis.onRejected),在状态变化(_resolve_reject 执行时)时调用。如此,便可以实现延迟调用、异步执行了。

B. 规则 No.6:onFulfilledonRejected 异步调用

根据 Promises/A+ 规则 3.1 的描述,我们使用 setTimeout 来保证 onFulfilledonRejected 的异步调用。

代码实现如下:

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.onFulfilledthis.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)

代码实现如下

1

IV. then 链式调用与值穿透

当 then 函数 return 了一个值,我们总能在下一个 then(onFulfilled)中取到,这就是 then 的链式调用

而如果前一个 then 函数没有传入任何参数(promise.then().then()),则可以在后面的 then 中获取之前 then 返回的值,此即 then 的值穿透

小结

  1. Promises/A+

  2. Promise 知识汇总和面试情况

  3. 面试官:“你能手写一个 Promise 吗”