实现一个符合Promise/A+标准的Promise

– What i can’t create, i don’t understant

前言

实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub

Promise标准

Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:

  • promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。
  • promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。
  • 不同的promise的实现需要可以相互调用

Promise构造函数

产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生Promise实现也是使用的构造函数,因此我也决定使用构造函数的方法。

首先,先写一个大概的框架出来:

1
2
3
4
5
6
7
8
9
// 众所周知, reject,用来改变promise的状态
function Promise(executor) {
this.status = 'pending'
this.value = void 0 // 为了方便把value和reason合并

const resolve = function() {}
const reject = function() {}
executor(resolve, reject)
}

很明显,这个构造函数还有很多问题们一个一个来看:

  • resolve和reject并没有什么卵用
    首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Promise(executor) {
    this.status = 'pending'
    this.value = void 0 // 为了方便把value和reason合并

    const resolve = value => {
    this.value = value
    this.status = 'resolved'
    }
    const reject = reason => {
    this.value = reason
    this.status = 'rejected'
    }
    executor(resolve, reject)
    }

    然后,当resolve或者reject调用的时候,需要执行在then方法里传入的相应的函数(通知)。有没有觉得这个有点类似于事件(发布-订阅模式)呢?

    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
    function Promise(executor) {
    this.status = 'pending'
    this.value = void 0 // 为了方便把value和reason合并

    this.resolveListeners = []
    this.rejectListeners = []

    // 通知状态改变
    const notify(target, val) => {
    target === 'resolved'
    ? this.resolveListeners.forEach(cb => cb(val))
    : this.rejectListeners.forEach(cb => cb(val))
    }

    const resolve = value => {
    this.value = value
    this.status = 'resolved'
    notify('resolved', value)
    }
    const reject = reason => {
    this.value = reason
    this.status = 'rejected'
    notify('rejected', reason)
    }
    executor(resolve, reject)
    }
  • status和value并没有做到一旦确定,无法更改。这里有两个问题,一是返回的对象暴露了status和value属性,并且可以随意赋值;二是如果在executor里多次调用resolve或者reject,会使value更改多次。

    第一个问题,如何实现只读属性:

    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
    function Promise(executor) {
    if (typeof executor !== 'function') {
    throw new Error('Promise executor must be fucntion')
    }

    let status = 'pending' // 闭包形成私有属性
    let value = void 0

    ......

    // 使用status代替this.value
    const resolve = val => {
    value = val
    status = 'resolved'
    notify('resolved', val)
    }
    const reject = reason => {
    value = reason
    status = 'rejected'
    notify('rejected', reason)
    }

    // 通过getter和setter设置只读属性
    Object.defineProperty(this, 'status', {
    get() {
    return status
    },
    set() {
    console.warn('status is read-only')
    }
    })

    Object.defineProperty(this, 'value', {
    get() {
    return value
    },
    set() {
    console.warn('value is read-only')
    }
    })

    第二个问题,避免多次调用resolve、reject时改变value,而且标准里(—3 it must not be called more than once)也有规定,then注册的回调只能执行一次。

    1
    2
    3
    4
    5
    6
    const resolve = val => {
    if (status !== 'pending') return // 避免多次运行
    value = val
    status = 'resolved'
    notify('resolved', val)
    }
  • then注册的回调需要异步执行。

    说到异步执行,对原生Promise有了解的同学都知道,then注册的回调在Micro-task中,并且调度策略是,Macro-task中执行一个任务,清空所有Micro-task的任务。简而言之,promise异步的优先级更高。

    其实,标准只规定了promise回调需要异步执行,在一个“干净的”执行栈执行,并没有规定一定说要用micro-task,并且在低版本浏览器中,并没有micro-task队列。不过在各种promise的讨论中,由于原生Promise的实现,micro-task已经成成为了事实标准,而且promise回调在micro-task中也使得程序的行为更好预测。

    在浏览器端,可以用MutationObserver实现Micro-task。本文利用setTimeout来简单实现异步。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const resolve = val => {
    if (val instanceof Promise) {
    return val.then(resolve, reject)
    }

    // 异步执行
    setTimeout(() => {
    if (status !== 'pending') return

    status = 'resolved'
    value = val
    notify('resolved', val)
    }, 0)
    }

最后,加上错误处理,就得到了一个完整的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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}

let status = 'pending'
let value = void 0

const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}

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

setTimeout(() => {
if (status !== 'pending') return

status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}

const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return

status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}

this.resolveListeners = []
this.rejectListeners = []

Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})

Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})

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

总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数

实现then方法

首先需要确定的是,then方法是写在构造函数里还是写在原型里。写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。

ok,还是先写一个大概的框架:

1
2
3
4
5
6
Promise.prototype.then = function (resCb, rejCb) {
this.resolveListeners.push(resCb)
this.rejectListeners.push(rejCb)

return new Promise()
}

随后,一步一步的完善它:

  • then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Promise.prototype.then = function (resCb, rejCb) {
    return new Promise((res, rej) => {
    this.resolveListeners.push((val) => {
    try {
    const x = resCb(val)
    res(x) // 以resCb的返回值为value来resolve
    } catch (e) {
    rej(e) // 如果出错,返回的promise以异常为reason来reject
    }
    })

    this.rejectListeners.push((val) => {
    try {
    const x = rejCb(val)
    res(x) // 注意这里也是res而不是rej哦
    } catch (e) {
    rej(e) // 如果出错,返回的promise以异常为reason来reject
    }
    })
    })
    }

    ps:众所周知,promise可以链式调用,说起链式调用,我的第一个想法就是返回this就可以了,但是then方法不可以简单的返回this,而要返回一个新的promise对象。因为promise的状态一旦确定就不能更改,而then方法返回的promise的状态需要根据then回调的运行结果来决定。

  • 如果resCb/rejCb返回一个promiseA,then返回的promise需要跟随(adopt)promiseA,也就是说,需要保持和promiseA一样的status和value。

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
this.resolveListeners.push((val) => {
try {
const x = resCb(val)

if (x instanceof Promise) {
x.then(res, rej) // adopt promise x
} else {
res(x)
}
} catch (e) {
rej(e)
}
})

this.rejectListeners.push((val) => {
try {
const x = resCb(val)

if (x instanceof Promise) {
x.then(res, rej) // adopt promise x
} else {
res(x)
}
} catch (e) {
rej(e)
}
})
  • 如果then的参数不是函数,需要忽略它,类似于这种情况:

    1
    2
    3
    new Promise(rs => rs(5))
    .then()
    .then(console.log)

    其实就是把value和状态往后传递

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    this.resolveListeners.push((val) => {
    if (typeof resCb !== 'function') {
    res(val)
    return
    }

    try {
    const x = resCb(val)

    if (x instanceof Promise) {
    x.then(res, rej) // adopt promise x
    } else {
    res(x)
    }
    } catch (e) {
    rej(e)
    }
    })

    // rejectListeners也是相同的逻辑

  • 如果调用then时, promise的状态已经确定,相应的回调直接运行

    1
    2
    3
    // 注意这里需要异步
    if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
    if (status === 'rejected') setTimeout(() => rejectCb(value), 0)

最后,就得到了一个完整的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
50
51
// resolveCb和rejectCb是相同的逻辑,封装成一个函数
const thenCallBack = (cb, res, rej, target, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}

try {
const x = cb(val)

if (x instanceof Promise) {
x.then(res, rej) // adopt promise x
} else {
res(x)
}
} catch (e) {
rej(e)
}
}

Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise

thenPromise = new Promise((res, rej) => {
/**
* 这里不能使用bind来实现柯里画,规范里规定了:
* --5: onFulfilled and onRejected must be called as functions
* (i.e. with no this value))
*/
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', val)
}

if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}

if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})

return thenPromise
}

不同的Promise实现可以互相调用

首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:

1
2
3
4
5
6
7
new MyPromise(rs => rs(5))
.then(val => {
return Promise.resolve(5) // 原生Promise
})
.then(val => {
return new Bluebird(r => r(5)) // Bluebird的promise
})

关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断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
const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}

if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}

if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}

let called = false
try {
// 这里有一个有意思的技巧.
// 如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
const then = x.then

if (typeof then !== 'function') {
return resolve(x)
}

then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}

到这里,一个符合标准的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
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
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}

let status = 'pending'
let value = void 0

const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}

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

setTimeout(() => {
if (status !== 'pending') return

status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}

const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return

status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}

this.resolveListeners = []
this.rejectListeners = []

Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})

Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})

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

const thenCallBack = (cb, res, rej, target, promise, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}

try {
const x = cb(val)
resolveThenable(promise, x, res, rej)
} catch (e) {
rej(e)
}
}

const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}

if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}

if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}

let called = false
try {
// 这里有一个有意思的技巧
// 如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
const then = x.then

if (typeof then !== 'function') {
return resolve(x)
}

then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}

Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise

thenPromise = new Promise((res, rej) => {
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', thenPromise, val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', thenPromise, val)
}

if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}

if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})

return thenPromise
}

测试脚本

关于promise的一些零散知识

  • Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
  • promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。

感想与总结

实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。