实现Promise/A+规范

初衷

为了了解Promise实现原理,查看了很多博客,并且翻阅了Promise/A+规范;由于很多实现都比较基础,并未详细介绍并处理很多细节,所以整理后,将自己的实现思路以及处理细节记录下来。

Promise简介

Javascript作为单线程机制,无法同时执行多个任务(当然,H5新增Web Worker可以在js线程外执行任务),所以异步编程的方式都是通过回调或者事件监听方式解决(具体可参考:阮一峰博客:Javascript异步编程的4种方法);这也造成多个Javascript异步任务的处理会出现“callback hell”(回调地狱),使得这部分代码难以维护,并且很难读懂。

所以,Javascript社区的大牛们就提出解决这种回调地狱的模式(Promise),最初的实现方案有jQuery的deferred,whenq等;现在,最流行的是 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
13
function 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
9
var 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
16
function 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
7
var 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
6
var 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
3
var 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
60
function 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)
}
}

构造函数定义时有几个重点:

  1. 构造函数参数resolver必须是一个函数,否则抛出错误
  2. resolve,reject实现中回调函数的调用必须异步执行(我的实现是直接在resolve中异步)
  3. resolve,reject方法必须确保只执行一次

Then函数—重中之重

关于Then方法的几点描述需要注意:

  • 1.Promise必须包含一个then方法以访问其当前值、终值和据因
  • 2.then方法接收两个参数:onFulfilled和onRejected
  • 3.then方法必须返回一个Promise对象(这个是实现Promise链式调用的关键)
简单实现并修改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
Promise.prototype.then = function (onFulfilled, onRejected) {
var promise = this
var promise2 = new Promise(function(resolve, reject) {
function handleResolve(value) {
var _value = typeof onFulfilled === 'function'
? onFulfilled(value)
: value
resolve(_value)
}

function handleReject(reason) {
var _reason = typeof onRejected ? onRejected(_reason) : reason
reject(_reason)
}
if (promise._status === PENDING) {
promise._resolveCallbacks.push(handleResolve)
promise._rejectCallbacks.push(handleReject)
} else if (promise._status === FULFILLED) {
handleResolve(promise._value)
} else if (promise._status === REJECTED) {
handleReject(promise._reason)
}
})
return promise2
}

处理步骤如下:

  1. 当调用then方法时,首先定义promise变量保存当前Promise实例
  2. 定义一个新的promise2(then方法返回的),实例化时判断promise(以下都指代当前Promise实例)的状态
  3. 如果是PENDING状态,则将onFulfilled、onRejected包装后的回调函数添加到promise的回调栈中
  4. 如果是FULFILLED,则将promise的当前value作为参数执行promise2的resolve(将promise的状态传递到promise2)
  5. 如果是REJECTED,则将promise的当前reason作为参数执行promise2的reject

根据Promise/A+规范,onFulfilled/onRejected函数的返回值不同,进行的处理过程是不同的,并且思考:

  • 链式调用过程中如何改变下个promise的状态
  • resolver函数的参数resolve如果处理的Promise实例与then方法中返回Promise的区别是什么,会产生怎样的影响

带着疑问,一起探讨下promise的处理过程

Promise处理过程

到目前为止的实现方案存在诸多问题,then函数根据规范需处理:
promise2 = promise1.then(onFulfilled, onRejected)

  1. onFulfilled/onRejected如果不是函数,需忽略,根据promise1的执行状态及结果执行promise2。(即promise1执行成功,则执行promise2的resolve,参数为promise1的结果值;反之亦然)
  2. 如果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
70
function 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
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
/**
* 生成promise执行栈函数,说明:
* e.g. promise2 = promise1.then(onFulfilled, onRejected)
* 根据Promise/A+规范,onFulfilled/onRejected依赖于上一个promise.then()返回的新的promise2
* 并且针对执行返回值定义了一系列处理流程,所以对onFulfilled/onRejected进行闭包封装处理函数并保存在上一个promise的执行栈中
* 即规范中的[[Resolve]](promise2, x)
* @param {Promise} promise promise
* @param {Any} callback onFulfilled/onRejected
* @param {'resolve'|'reject'} action 执行状态
* @return {Function} resolve/reject回调函数
*/
function makeCallback (promise, callback, action) {
// 以下注释x均表示onFulfilled/onRejected函数返回值
return function (value) {
var x = null
// onFulfilled 和 onRejected 都是可选参数,如果不是函数会被忽略,不添加到执行栈中。
// 并且继承上一个promise的执行状态及执行结果
if (isFunction(callback)) {
try {
x = callback(value)
} catch (error) {
reject(promise, error)
}

solveProcess(promise, x, action, true)
} else {
action === 'resolve'
? resolve(promise, value)
: reject(promise, value)
}
}
}

重构then方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.prototype.then = function (onFulfilled, onRejected) {
var promise = new Promise(function() {})

switch (this._status) {
case PENDING:
this._resolveCallback.push(makeCallback(promise, onFulfilled, 'resolve'))
this._rejectCallback.push(makeCallback(promise, onRejected, 'reject'))
break
case FULFILLED:
makeCallback(promise, onFulfilled, 'resolve')(this._value)
break
case REJECTED:
makeCallback(promise, onRejected, 'reject')(this._reason)
break
default:
break
}

return promise
}

注意:_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

参考