– 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 | // 众所周知, reject,用来改变promise的状态 |
很明显,这个构造函数还有很多问题们一个一个来看:
resolve和reject并没有什么卵用
首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:1
2
3
4
5
6
7
8
9
10
11
12
13
14function 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
26function 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
40function 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
6const 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
14const 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 | function Promise(executor) { |
总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数
实现then方法
首先需要确定的是,then方法是写在构造函数里还是写在原型里。写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。
ok,还是先写一个大概的框架:
1 | Promise.prototype.then = function (resCb, rejCb) { |
随后,一步一步的完善它:
then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Promise.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 | this.resolveListeners.push((val) => { |
如果then的参数不是函数,需要忽略它,类似于这种情况:
1
2
3new 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
18this.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 | // resolveCb和rejectCb是相同的逻辑,封装成一个函数 |
不同的Promise实现可以互相调用
首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:
1 | new MyPromise(rs => rs(5)) |
关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断promise的地方
1 | const resolveThenable = (promise, x, resolve, reject) => { |
到这里,一个符合标准的Promise就完成了,完整的代码如下:
1 | function Promise(executor) { |
关于promise的一些零散知识
- Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
- promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。
感想与总结
实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。