如何实现一个 better than Promises/A+ 规范的 promise 底层类库

背景

提起Promise,大家并不陌生。Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件 )更合理和更强大。它由前端社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。然而对于 IE,无论是 IE6-10,还是最新版本的 IE11,都不支持 Promise,或许MS IE认为前端程序员可以自己轻松去实现这个标准,又或许不认同已形成统一的规范,认为它可能存在一些缺点。但是无论怎样,Promise的出现给前端带来了里程碑式的变化,解决了回调地域的问题,进而 async await的出现让前端代码前所未有的优雅。但是,如果要支持 IE(起码要支持最新版的 IE11),我们该如何做?当然我们可以去使用别人写好的 pollyfill,但是不无前车之鉴,别人的代码不可能是永久安全的,更何况如此核心的一个 API,可能由于信息安全等方面的因素我们不能去随意引用别人的类库。所以本文将阐述如何去实现一个遵循 Promises/A+规范的 Promise 类库,不仅做好了对 IE 的支持,又在理解 Promise 原理的同时,提升了自身的编程思想和能力。

前置

Promises/A+,建议通篇阅读,Promises/A+是在Promises/A的基础上对原有规范进行修正和增强。Promises/A+组织会因新发现的问题以向后兼容的方式修改规范,且每次修改均会经过严格的考虑、讨论和测试,因此Promises/A+规范相对来说还是比较稳定的。

实现

  1. 首先 Promise 是一个类,我们可以通过 new 来创建 Promise 实例。参数是 executor,顾名思义,这是一个执行器且会立即执行。
  2. executor 有两个参数 resolve 和 reject,resolve 代表成功的回调,reject 代表失败的回调。
  3. Promise 默认有三个状态: 等待,成功,失败,默认为等待。调用 resolve 变为成功,调用 reject 变为失败。
  4. 返回的实例上有一个 then 方法。then 中有两个参数:成功对应的函数和失败对应的函数。
  5. 如果同时调用成功和失败,默认会采用第一次调用的结果。
  6. 抛出错误会走失败逻辑。
  7. 成功时可以传入成功的值(value),失败时可以传入失败的原因(reason)。
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
// 枚举三种状态
const ENUM_STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}

class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
}
}
try {
executor(resolve, reject) // 立即执行
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
if (this.status === ENUM_STATUS.FULFILLED) {
onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
onRejected(this.reason)
}
}
}

这就结束了吗?不,以上才刚刚开始,实现了一个“乞丐版”的 Promise。仔细看我们会发现如下问题:
调用 then 方法时,如果加入了定时器,会存在很大问题:因为没有考虑到异步的情况,调用 then 时并没有拿到成功或者失败的状态,所以会一直处于 pending 状态。多次调用 then 时存在异步又如何处理?所以,当我们调用 then 时如果是 pending 状态,则需要把成功和失败的回调分别进行保存。调用 resolve 时,将保存的函数进行执行。这是一个发布-订阅模式,可以加深理解,具体实现如下:

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
class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

// 创建成功和失败的队列用于分别存放
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value

// 成功时执行队列中的方法
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason

// 失败时执行队列中的方法
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
if (this.status === ENUM_STATUS.FULFILLED) {
onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
onRejected(this.reason)
}
if (this.status === ENUM_STATUS.PENDING) {
// 便于扩展 采用AOP模式
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
}
}

以上代码加入了异步处理的逻辑,同时实现了 then 方法可以进行多次调用,但是目光敏锐的同学会问:既然可以多次调用,那可以进行链式调用吗?确实在 Promise 中,then 的链式调用是 Promise 的核心之一,正是因为支持链式调用从而解决了无限回调的问题,更多细节如下:

  1. 可以在 then 方法中的成功回调和失败回调抛出异常,走到下一个 then 方法的失败回调中。
  2. 如果返回的是 Promise, 则会把当前 Promise 的状态作为结果向下传递。
  3. 错误处理遵循就近原则,向下找到即执行。

对于 then 方法链式调用的思考

比如jQuery链式调用是通过return this来实现的,但是对于 Promise 来说这样是行不通的。Promise 是通过返回一个新的 Promise 来实现的。原因是什么呢?我们知道 Promise 有一个重要的特点是:如果成功则不能失败,如果失败则不能成功。打个比方:现有一段有 5 个 then 方法调用的代码片段,如果 Promise 使用同一个实例(return this),第一个 then 抛出异常,则会直接进入失败的方法中(catch),其余的 then 均没有被执行。但是如果 then 返回的是一个新的 Promise,这样既可以成功也可失败。

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
class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
let x = onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
let x = onRejected(this.reason)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason)
})
}
})
return promise2
}
}

2.2.7 then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);
2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [Resolve].
2.2.7.2 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

如上代码修改,根据Promises/A+规范所述(2.2.7),定义了then方法返回的promise2和成功或失败的返回值x, 这个x需要通过resolve或者reject进行处理,所以要知道x是成功还是失败,需要一个方法进行判断,我们新增了resolvePromise()方法。

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
then (onFulfilled, onRejected) { 
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
}
if (this.status === ENUM_STATUS.REJECTED) {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
})
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
})
}
})
return promise2
}

const resolvePromise = (x, promise2, resolve, reject) => {}

如下,则要实现resolvePromise方法,来解析xpromise2的关系,但是这样写还会有一个问题,仔细看会发现我们调用resolvePromise方法时的参数传入了promise2,但是我们对该方法的调用是写在promise2实例化方法体里的,这样会导致执行时报错,提示我们不能在promise2初始化完成前使用promise2。文档里也有讲:

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.

我们可以使用宏任务setTimeout去进行包装,使onFulfilled和onRejecte都可以异步执行。既然用宏任务包装了一层,错误处理肯定也会丢失,所以这里我们在用setTimeOut的同时,也要使用try catch进行包装。

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
then (onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return promise2
}

继续实现resolvePromise方法,这里有一点要注意的是,我们需要兼容其他版本或者其他人写的promise,虽然都是按照Promises/A+规范来实现的,但其中还是有一定的差异。需要注意的另外一点是规范中指出:如果promisex指向同一个object,则通过reject promise抛出一个类型错误作为失败的原因。

这里将规范文档中的重点筛出来,作为代码实现的注释:

2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
2.3.2 If x is a promise, adopt its state.
2.3.3 Otherwise, if x is an object or function,
2.3.3.1 Let then be x.then.
2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
2.3.3.3 If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
2.3.3.3.1 If/when resolvePromise is called with a value y, run [Resolve].
2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
2.3.3.3.4 If calling then throws an exception e,
2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
2.3.3.4 If then is not a function, fulfill promise with x.
2.3.4 If x is not an object or function, fulfill promise with x.

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
const resolvePromise = (x, promise2, resolve, reject) => {
if (x === promise2) {
// 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
// 我买几个橘子去。你就在此地,不要走动。
reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 兼容其他人的Promise,则Promise可能是对象也可能是函数。
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// 2.3.3.3.3 此处解析的x如果是Promise,则可能是其他类库的Promise, 即存在一种不确定性: 此Promise既调用了成功也调用了失败。所以需要定义变量called来规避此种情况。
let called
try {
// 2.3.3.1 Let then be x.then.
// 取 then 方法
let then = x.then

// then的类型如果是function, 则then是Promise
if (typeof then === 'function') {
// 2.3.3.3 If then is a function, call it with x as this.
// 取then方法的目的是在此处进行复用,防止多次调用get方法(x.then)而导致报错。
// 2.3.3.3.1 --- 2.3.3.3.3 (成功 => y 失败 => r)
then.call(x, y => {
if (called) return
called = true
// y也可能是Promise,则需递归解析y的值直到结果为普通值。
resolvePromise(y, promise2, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
// 2.3.3.4 If then is not a function, fulfill promise with x.
// 非Promise的普通对象
resolve(x)
}
} catch (e) {
// 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
if (called) return
called = true
reject(e)
}
} else {
// 2.3.4 If x is not an object or function, fulfill promise with x.
// 非Promise的普通值
resolve(x)
}
}

至此,我们完整实现了resolvePromise方法。

回头看文档

2.2.1 Both onFulfilled and onRejected are optional arguments:
2.2.1.1 If onFulfilled is not a function, it must be ignored.
2.2.1.2 If onRejected is not a function, it must be ignored.

可以了解到then方法的两个参数onFulfilledonRejected均为可选参数,我们实现时则默认写成了必传的方式,所以此处需要修改,则涉及到一个“穿透”的概念。可以用如下例子进行理解:

1
2
3
4
5
new Promise((resolve, reject) => {
resolve('foo')
}).then().then().then(data => {
console.log(data)
})

如果用我们目前实现的Promise,很明显data的值无法正常打印,所以我们需要进行改写,通过中间的两个then()进行值的穿透,传递到最后一个then中:

1
2
3
4
5
new Promise((resolve, reject) => {
resolve('foo')
}).then(data => data).then(data => data).then(data => {
console.log(data)
})

那么,思路来了:

1
2
3
4
5
6
7
8
9
then (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

// 此处省略 Promise2的定义
// ...

return promise2
}

这样,我们完整地实现了Promise。接下来需要检测我们实现的Promise是否“扛揍”,Promises/A+文档最后贴上了推荐测试的包:Compliance Tests

测试要求提供一个测试入口,按要求添加(测试完毕后会对此方法进行解读)

1
2
3
4
5
6
7
8
Promise.defer = Promise.deferred = () => {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

全局安装promises-aplus-tests

1
npm install -g promises-aplus-tests

安全完毕后我们进行测试:

1
promises-aplus-tests promise.js

最后提示:872 passing (16s), 代表测试全部通过。部分截图如下:
promise-test-result.png

接下来,我们解释一下这个延迟对象deferred: Promise的实现,本身是为了解决嵌套问题。比如我们开始封装一个异步方法foo,如果我们在foo方法体中直接new Promise,这样直接开始了一层嵌套,很不友好。为了简化此种写法,让我们封装的方法可以直接promise化调用foo.then(),所以我们可以使用Promise.defer这个静态方法(通过类本身来调用的方法),此方法会返回一个dfd。即在foo方法体开始定义let dfd = Promise.defer(), 方法体结尾return dfd.promise,在foo方法体中的异步方法处理成功失败均可挂载到dfd上:dfd.resolve(data);dfd.reject(error)。如此我们减少了不必要的嵌套,让代码更加优雅。

我们需要对Promise的静态方法catch进行实现,很容易想到:catch相当于一个没有成功的then方法,如下:

1
2
3
catch (errorCallback) {
return this.then(null, errorCallback)
}

另外,还有两个静态方法Promise.resolve()Promse.reject(), resolve方法即产生一个成功的Promise, reject方法即产生一个失败的Promise:

1
2
3
4
5
6
7
8
9
10
11
static resolve (value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}

static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}

此处如果经过测试会发现一个小问题,这个静态方法resolve的参数value如果是一个Promise(调用了new Promise),在其中的异步方法中又调用了resolve方法,我们就无法拿到正确的结果(不会等待异步方法执行完毕,直接返回Promise且会处在pending状态) 。所以我们就需要对这个value进行递归解析(ES6内部自身已实现了该方法):

1
2
3
4
5
6
7
8
9
10
11
12
// 重写constructor中的resolve方法
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}

if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}

综上,我们实现了Promise.resolve的等待功能。

至此,我们不仅完美实现了符合Promises/A+规范的Promise类库、通过了官方的测试。 还为了对标ES6 Promise, 让Promise更完整,实现了静态方法resolve,rejectcatch等方法。

后续计划:

  1. Promise.all,Promise.race,Promise.finally进行实现
  2. Promise.allSettled(ES2020)进行实现

最后,贴上完整的代码:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const ENUM_STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}

const resolvePromise = (x, promise2, resolve, reject) => {
if (x === promise2) {
// 我买几个橘子去。你就在此地,不要走动。
reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let called
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, y => {
if (called) return
called = true
resolvePromise(y, promise2, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}

class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING

this.value = undefined
this.reason = undefined

this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}

if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}

try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}

then (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})

return promise2
}

catch (errorCallback) {
return this.then(null, errorCallback)
}

static resolve (value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}

static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
}

Promise.defer = Promise.deferred = () => {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

module.exports = Promise