初衷
为了了解Promise实现原理,查看了很多博客,并且翻阅了Promise/A+规范;由于很多实现都比较基础,并未详细介绍并处理很多细节,所以整理后,将自己的实现思路以及处理细节记录下来。
Promise简介
Javascript作为单线程机制,无法同时执行多个任务(当然,H5新增Web Worker可以在js线程外执行任务),所以异步编程的方式都是通过回调或者事件监听方式解决(具体可参考:阮一峰博客:Javascript异步编程的4种方法);这也造成多个Javascript异步任务的处理会出现“callback hell”(回调地狱),使得这部分代码难以维护,并且很难读懂。
所以,Javascript社区的大牛们就提出解决这种回调地狱的模式(Promise),最初的实现方案有jQuery的deferred,when,q等;现在,最流行的是 Promise/A+ 规范。
这里不再赘述Promise/A+规范,详细可查看文章底部链接。
Promise的使用
详见…
Promise 简单实现
构造函数和then
首先实现一个最简化版的功能,在使用Promise时,会传入我们的异步操作,并且默认有两个参数 resolve 和 reject ;这里我们先实现Promise resolve和then的简单功能:1
2
3
4
5
6
7
8
9
10
11
12
13function Promise(resolver) {
this.callback = function() {}
function resolve(value) {
this.callback(value)
}
resolver(resolve.bind(this))
}
Promise.prototype.then = function (fn) {
this.callback = fn
}
上面定义的构造函数中,实例化Promise并添加then方法时,会将then方法的参数保存为回调,以保证resolve时执行回调,现在测试一下:1
2
3
4
5
6
7
8
9var promise = new Promise(function (resolve) {
setTimeout(function() {
resolve('resolve')
}, 100)
})
promise.then(function(res) {
console.log(res)
})
// 100ms后打印'resolve'
注意:上面的构造函数存在一个问题,使用原生Promise时我们并不总是传递异步函数,也可能是同步的,这个时候我们上面的then方法就会不执行[1],这个后面讨论。
链式调用
链式调用的实现就是在then方法中返回this自身就可以了,首先我们需要将回调函数改为数组,用来存储每次执行then时的回调函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function Promise(resolver) {
this._resolveCallbacks = []
function resolve(value) {
this._resolveCallbacks.forEach(function(callback) {
callback(value)
})
}
resolver(resolve.bind(this))
}
Promise.prototype.then = function (fn) {
this._resolveCallbacks.push(fn)
return this
}
测试:1
2
3
4
5
6
7var promise = new Promise(function(resolve) {
setTimeout(function() {resolve('resolve')},0)
})
.then(res => console.log(res))
.then(res => console.log(res))
// 'resolve'
// 'resolve'
这样子就实现了,但是存在问题就是同一个Promise实例的所有then回调执行时的value都是同一个值,链式then回调中返回新值无法在下一个回调中使用[2]。1
2
3
4
5
6var promise = new Promise(function(resolve) {
setTimeout(function() {resolve('resolve')},0)
})
.then(res => {console.log(res); return 'new resolve'})
.then(res => console.log(res))
// 即最后一个then执行的也是'resolve',并不符合Promise预期
========================== 分界线 ==============================
Promise具体实现
下面针对Promise/A+规范对具体细节处理过程进行解剖
Promise状态
根据Promise/A+规范,Promise应该有三个状态:
- Pending(等待)
- Fulfilled(满足/执行) 当满足时,当前Promise必须有一个终值(value)
- Rejected(拒绝) 当拒绝时,必须有一个拒因(reason)
Pending状态可以转变为Fulfilled或者Rejected;Fulfilled/Rejected不可以再转变为其他任何状态(状态不可逆);
所以我们定义三个状态:1
2
3var PENDING = 0
var FULFILLED = 1
var REJECTED = 2
接下来,引入状态机制并加入reject状态回调;初始promise的状态为PENDING,当resolve时将状态更改为FULFILLED,reject时更改为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
53
54
55
56
57
58
59
60function Promise(resolver) {
// resolver必须是一个函数,与es6实现相同
if (!isFunction(resolver)) {
throw new TypeError('TypeError: Promise resolver ' + resolver.toString() + ' is not a function')
}
this._status = PENDING
this._value = undefined
this._reason = undefined
this._resolveCallbacks = []
this._rejectCallbacks = []
// new Promise((resolve, reject) => {})
// 立即调用实例化时的参数,并传递resolve和reject参数
resolver(
// 即传递给构造函数参数的resolve参数
function (value) {
// Promise需要确保then函数的onFulfilled与onRejected函数异步执行
// 即针对角标[1]的问题,就算promise构造函数参数不是异步执行,
// 也必须确保在resolve/reject执行时then回调函数已经添加到回调栈中(即_resolveCallbacks/_rejectCallbacks)
setTimeout(function () {
// 该处理过程后面会讲,这里暂时可以理解为调用resolve方法
solveProcess(promise, value)
}, 0)
},
function (reason) {
setTimeout(function () {
reject(promise, reason)
}, 0)
}
)
}
// resolve方法
function resolve (promise, value) {
// 确保resolve只能执行一次
if (promise._status) {
return
}
promise._status = FULFILLED
promise._value = value
for (var index = 0, len = promise._resolveCallback.length; index < len; index++) {
promise._resolveCallback[index](value)
}
}
// reject方法,改变promise状态,执行回调
function reject (promise, reason) {
if (promise._status) {
return
}
promise._status = REJECTED
promise._reason = reason
for (var index = 0, len = promise._rejectCallback.length; index < len; index++) {
promise._rejectCallback[index](reason)
}
}
构造函数定义时有几个重点:
- 构造函数参数resolver必须是一个函数,否则抛出错误
- resolve,reject实现中回调函数的调用必须异步执行(我的实现是直接在resolve中异步)
- resolve,reject方法必须确保只执行一次
Then函数—重中之重
关于Then方法的几点描述需要注意:
- 1.Promise必须包含一个then方法以访问其当前值、终值和据因
- 2.then方法接收两个参数:onFulfilled和onRejected
- 3.then方法必须返回一个Promise对象(这个是实现Promise链式调用的关键)
简单实现并修改then方法:
1 | Promise.prototype.then = function (onFulfilled, onRejected) { |
处理步骤如下:
- 当调用then方法时,首先定义promise变量保存当前Promise实例
- 定义一个新的promise2(then方法返回的),实例化时判断promise(以下都指代当前Promise实例)的状态
- 如果是PENDING状态,则将onFulfilled、onRejected包装后的回调函数添加到promise的回调栈中
- 如果是FULFILLED,则将promise的当前value作为参数执行promise2的resolve(将promise的状态传递到promise2)
- 如果是REJECTED,则将promise的当前reason作为参数执行promise2的reject
根据Promise/A+规范,onFulfilled/onRejected函数的返回值不同,进行的处理过程是不同的,并且思考:
- 链式调用过程中如何改变下个promise的状态
- resolver函数的参数resolve如果处理的Promise实例与then方法中返回Promise的区别是什么,会产生怎样的影响
带着疑问,一起探讨下promise的处理过程
Promise处理过程
到目前为止的实现方案存在诸多问题,then函数根据规范需处理:promise2 = promise1.then(onFulfilled, onRejected)
- onFulfilled/onRejected如果不是函数,需忽略,根据promise1的执行状态及结果执行promise2。(即promise1执行成功,则执行promise2的resolve,参数为promise1的结果值;反之亦然)
- 如果onFulfilled/onRejected返回一个值x,则按照规范的解决过程进行处理
[[Resolve]](promise2, x)
。并且处理结束后promise2成功执行(即promise2的resolve),注意:只有抛出异常时才会执行promise2的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
66
67
68
69
70function solveProcess (promise, x) {
// 1.判断返回值x是否指向了promise,避免死循环,抛出异常
if (x === promise) {
// x与初始promise为同一个Promise,会造成死循环
var reason = new TypeError('TypeError: A promises callback cannot return that same promise.')
reject(promise, reason)
}
// 2.x是否为Promise实例,如果是,则根据x的执行结果,promise执行对应的resolve/reject
else if (x instanceof Promise) {
// 如果x为Promise,则父Promise等待其fulfilled或reject
x.then(
function (value) {
resolve(promise, value)
},
function (error) {
reject(promise, error)
}
)
} else {
// 3.如果以上两点都不是则判断x是否为thenable函数或对象
tryThenable(promise, x, action, isCallback)
}
}
function tryThenable (promise, x) {
// 4.x是否对象或函数,如果不是,则以x为值,调用resolve()
if (objectOrFunction(x)) {
var then = null
// 5.获取x.then,如果取出异常则reject promise
try {
then = x.then
} catch (error) {
reject(promise, error)
}
// 6.x为thenable则执行then方法(相当于处理Promise)
// 否则以x为值,调用resolve()
if (isFunction(then)) {
// 7.then方法的resolvePromise和rejectPromise只允许执行一次,sealed做限制
// 如果rejectPromise执行了,则结束promise,并以r为据因
// 如果resolvePromise被调用则以参数y继续执行处理过程
var sealed = false
try {
then.call(
x,
/* eslint-disable no-undef */
function resolvePromise (y) {
if (sealed) return
sealed = true
solveProcess(promise, y)
},
function rejectPromise (r) {
if (sealed) return
sealed = true
reject(promise, r)
}
)
} catch (error) {
reject(promise, error)
}
} else {
resolve(promise, x)
}
} else {
resolve(promise, x)
}
}
function isFunction (fn) {
return typeof fn === 'function'
}
代码中第7点处理步骤与第2点处理步骤有区别,虽然都有then方法,但是第二点中x已经为Promise实例了,所以resolvePromise中直接调用了resolve方法,如果该实例resolve了一个新的Promise,则会等待执行结果后才会继续执行;第7点中x不是Promise实例,所以需要不停的手动调用处理过程,因为他并不会等待内部thenable的执行结果。
到这里,可以联系构造函数resolve函数,其实处理过程就这个
再看上面我提出的问题,resolve如果参数是promise1则当前promise需等待promise1执行完成,promise的状态继承promise1的状态并执行相应回调
增加onFulfilled/onRejected回调生成函数
1 | /** |
重构then方法
1 | Promise.prototype.then = function (onFulfilled, onRejected) { |
注意:_resolveCallback与_rejectCallback的回调函数中的promise实例是当前then返回的promise,这样才能实现链式调用,每次then方法返回一个新的Promise,根据上一个Promise执行状态执行对应的回调。这样也解决了上面角标[2]的问题,每个then函数都依赖于上一个then方法的执行结果。
同时,上面提出的问题,then函数返回promise实例其实可以改变下一个promise实例的处理状态,而resolve参数为promise则决定了当前promise的状态
总结
promise的实现包含了很多细节性问题,需要慢慢推敲实现,并且规范中并未描述具体的api实现细节,所以需要查看ecma规范。
对于api并未做详细的讲解,其实catch方法就是then(null, onRejected),Promise.all()等api实现起来也不太复杂。
具体api的实现可以查看我的promise实现:https://github.com/fengdonglp/fd-promise