理解和实现Promise

Promise的横空出世成功解决了困扰前端开发多年的“回调地狱”的问题,由社区提出并发展后,成功“转正”写入了ES6的语法标准中。虽然时刻在用,但是总感觉自己对它是既熟悉又陌生。于是自己怀着“为什么要这么写”和“这是如何实现的”的疑惑,研究了Promise的规范和尝试了自己实现,同时在此做一个笔记。

Promises/A+标准

目前主流的浏览器已经默认开起了对Promise的支持,在浏览器受限的情况下,社区也有大量的三方库,例如Qbluebird, rsvp支持。事实上,这些三方支持库都是基于Promises/A+标准来实现的。

术语

  1. promise是一个对象或函数,包含一个名叫then的方法,then方法的行为符合定义(定义见下文)。
  2. thenable是一个对象或者函数,定义了then方法。
  3. value是任意的js合法对象,包含undefiendthenable或者promise
  4. exception是指代码中抛出的异常。
  5. reason代表promise被rejected的原因。

状态

promise有3种状态:pending(等待),fulfilled(完成)以及rejected(拒绝)

  1. 当状态处于pending时,promise可以切换状态到fulfilled和rejected,但不是必须。
  2. 当状态处于fulfilled,必须有一个value(undefined也算),promise不能再切换到其他状态。
  3. 当状态处于rejected,,必须有一个reason,promise不能再切换到其他状态。

then

promise必须提供一个then方法,接受2个参数:

1
promise.then(onFulfilled?, onRejected?)

两个参数遵循以下规则:

  1. 2个参数都是可选的,如果不传,则被忽略。
  2. 如果onFulfilled是一个方法,promise被fulfilled后必须被执行,promise的value会成为onFilfilled的第一个参数传入。需要注意的是onFulfilled只允许调用一次。
  3. 如果onRejected是一个方法,promise被rejected后必须被执行,promise的reason会成为onFilfilled的第一个参数传入。需要注意的是onRejected只允许调用一次。
  4. onFulfilled和onReject必须在当前执行的上下文完成后才执行,通俗点说就是 必须是异步调用 ,在不同的平台上,应该有不同的实现,但是都有一个原则:asap(as soon as possible)。
  5. onFulfilled和onReject执行时应该作为全局函数调用,即不能绑定this对象。
  6. 同一个promise的then可以多次调用,当fulfilled或者rejected时,应该按照绑定的顺序执行(这里不是指链式调用,这是针对同一个promise)。
  7. then必须返回一个promise。通常来说返回的是一个新的promise对象,但不排除有些库的实现不同。

    1
    promise2 = promise1.then(onFulfilled, onRejected);
  8. 当onFulfilled或者onRejected返回了一个值x,那么这个x都会进入promise2的 解析过程Resolve(promise2, x)

  9. 当onFulfilled或者onRejected抛出了一个exception,那么promise2会以这个exception被reject。
  10. 如果promise1被以x值被fulfilled,但是没有传入onFilfilled回调,那么promise2会以x值被fulfilled。
  11. 如果promise1被以reason被rejected,但是没有传入onRejected回调,那么promise2会以reason被fulfilled。

解析过程

解析Resolve(promise, x)遵循以下规则:

  1. 如果promise === x,抛出一个TypeError
  2. 如果x是一个promise对象(这里的promise不一定是原生promise,有then方法实现都可算,因为不同的实现库可能混用),则必须等待x被fulfilled或者rejected。如果fulfilled,promise会以相同value被fulfilled。如果rejected,promise会以相同reason被rejected。

以上是几种常用情况,有兴趣的同学可以在the-promise-resolution-procedure了解更多信息。

与原生Promise

众所周知ES6已经原生提供了Promise对象,那ES6的Promise的实现也是遵循Promises/A+标准的吗?

官方也针对此做了说明:

The ECMAScript specification includes a section titled “Promise Objects”. This section mandates that a conformant implementation of ECMAScript have a Promise global. Largely due to the actions of the Promises/A+ community, the Promise global specified by ECMAScript and present in any conforming JavaScript engine is indeed a Promises/A+ implementation!

最后一句很重要:目前而言,任何Javascript引擎原生提供的Promise对象都是Promises/A+标准的实现!

所以,在兼容性方面,大家在使用时是无需担心的。无论是框架内置,还是babel转码,亦或三方实现,都是可以混合使用的。

这里可以查看所有Promises/A+标准实现的三方库

catch去哪了

看到这里,我们发现平时常用的catch方法并没有提及,光一个then方法的promise感觉都不完整了。事实上catch并不是Promises/A+标准的一部分,只是promise的一个扩展方法,在下面的实现中,我们会发现实现一个catch是非常简单的。

实现Promise

Promises/A+并没有约束promise创建形式,但是我们还是以ES6的形式为实现目标。

请忽视我使用ES6的语法来实现ES6的Promise😅。

构造函数

我们先声明一个类(构造函数),像原生Promise类那样传入一个executor函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyPromise {
constructor(executor) {
this._child = null;
this._deferred = [];
this._status = 0; // 0:pending 1:fulfilled 2:rejected
this._value = null;

try {
// executor执行是同步的,防止回调绑定this
executor(this._onfulfilled.bind(this), this._onRejected.bind(this));
} catch (error) {
this._onReject(error);
}
}
// ...
}

属性:

  • _child - 下一个promise的引用
  • _deferred - 延迟执行的队列,即在同一个promise绑定的多个then回调
  • _status - 当前promise的状态
  • _value - promise的valuereasonexception

我们将onfilfulled和onRejected回调传入executor函数中,并用try catch捕获executor抛出的异常。需要注意的是,executor是在构造时 同步 执行的。

不要被这里回调的命名所迷惑,其实onfilfulled和onRejected传入excutor后,就是我们通常用的resovlereject

1
new Promise((resolve, reject)=>{//...})

添加then方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
then(onFulfilled, onRejection) {
const child = new this.constructor(noop);
this._child = child;

this._deferred.push({
fulfill: onFulfilled || null,
reject: onRejection || null,
});

if (this._status !== 0) {
this._handle();
}

return child;
}
// ...

我们在then方法中构建了一个新的promise对象,并将其建立与_child的引用,并将传入的回调存入_deferred队列。

如果当前promise的状态已经不是pending,那么就立刻处理队列。

处理状态

下面是我们处理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
// ...

_onfulfilled(value) {
// 防止多次触发
if (this._status !== 0) return;

// 返回的是promise,要等待完成
if (value && value.then) {
value.then((val) => {
this._onfulfilled(val);
}, this._onRejected);
return;
}

this._dofulfilled(value);
}

_dofulfilled(value) {
this._status = 1;
this._value = value;
asap(() => {
this._handle();
});
}

_onRejected(reason) {
// 防止多次触发
if (this._status !== 0) return;

this._doRejected(reason);
}

_doRejected(reason) {
this._status = 2;
this._value = reason;
asap(() => {
this._handle();
});
}

_handle() {
const value = this._value;

if (this._deferred.length === 0 && this._status === 2) {
console.warn('未处理的promise异常', value);
return;
}

for (let i = 0, l = this._deferred.length; i < l; i++) {
const cb = this._status === 1 ? this._deferred[i].fulfill : this._deferred[i].reject;
// 没有回调,直接将当前值抛给下一个promise
if (!cb) {
if (this._status === 1) {
this._resolve(value);
} else {
this._reject(value);
}
} else {
try {
const res = cb(value);

if (res === this._child) {
throw new TypeError('无法将下一个promise作为值返回,因为会引起循环调用');
}

// 假设这样可以断定这是一个promise
if (res && res.then && typeof res.then === 'function') {
try {
res.then(this._onfulfilled.bind(this._child), this._onRejected.bind(this._child));
} catch (error) {
this._reject(error);
}
} else {
// 直接resolve给下一个promise
this._resolve(res);
}

} catch (error) {
// 尝试把错误抛给下一个promise
this._reject(error);
}
}
}
this._deferred = [];
}

_resolve(value) {
if (this._child) {
this._child._onfulfilled(value);
}
}

_reject(reason) {
if (this._child) {
this._child._onRejected(reason);
}
}
// ...

当我们调用resolve或者reject会触发promise的_onFilfulled和_onRejected方法,改变了当前promise的_status_value,然后在_handle里处理绑定的_deferred队列。

上面我们在介绍then时提到过,onFulfilled和onRejected必须是异步调用的,但是我们也需要保证它尽快执行,于是我们添加了上述代码中的asap方法。

1
2
3
// as soon as possible
const asap = (typeof process !== 'undefined' && process.nextTick)
|| setImmediate || setTimeout; // 还有个MutationObserver,但还未了解

我们在_handle方法内将_deferred队列每一项顺序执行。按照上文提到的解析过程处理细节。

到现在,我们的promise基本上可以正常工作了。之前我们提到过,Promises/A+标准并没定义then以外其他方法,但是在实际开发中,我们往往还需要添加额外的方法提升效率。

添加catch

一般来说我们更喜欢使用单独的catch来处理调用链上的错误。

1
2
3
4
5
//...
catch(onRejected){
return this.then(null, onRejected);
}
//...

是不是超级简单。

添加finally

在某些场景,比如从接口获取数据时我们开启了加载提示器,无论成功或者失败,我们都要关闭加载提示器。没人喜欢在thencatch里面重复写关闭的代码,于是我们添加一个finally方法处理所有状态。

1
2
3
4
5
6
//...
finally(onFinally){
// onFinally 不传入任何值
return this.then(() => onFinally(), () => onFinally());
}
//...

添加Promise.resolve和Promise.reject

1
2
3
4
5
6
7
8

MyPromise.resolve = function (value) {
return new MyPromise(resolve => resolve(value));
}

MyPromise.reject = function (reason) {
return new MyPromise((resolve, reject) => reject(reason));
}

添加Promise.all

这个方法可以让一组promise全部完成后再返回结果,这一组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
MyPromise.all = function () {
const promiseArr = arguments[0];
if (!Array.isArray(promiseArr)) {
throw new Error('需要传入数组');
}

const result = new Array(promiseArr.length);

return new MyPromise((resolve, reject) => {
let done = false;
let count = 0;

function setResult(index, value) {
if (done) return;
count++;
result[index] = value;
if (count === promiseArr.length) {
resolve(result);
done = true;
}
}

function setError(reason) {
if (done) return;

done = true;
reject(reason);
}

promiseArr.forEach((promise, index) => {
// 即使不是promise也包装成promise
MyPromise.resolve(promise)
.then((value) => {
setResult(index, value);
})
.catch((reason) => {
setError(reason)
});
});
});
}

添加Promise.race

使一组promise成为竞态关系,只返回第一个返回的promise的结果(无论是fulfilled还是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

MyPromise.race = function () {
const promiseArr = arguments[0];
if (!Array.isArray(promiseArr)) {
throw new Error('需要传入数组');
}

return new MyPromise((resolve, reject) => {
let done = false;

function setResult(value) {
if (done) return;

done = true;
resolve(value);
}

function setError(reason) {
if (done) return;

done = true;
reject(reason);
}

promiseArr.forEach((promise) => {
MyPromise.resolve(promise)
.then((value) => {
setResult(value);
})
.catch((reason) => {
setError(reason)
});
});
});
}

例如,我们希望对一个异步任务设置超时时间,那么我们可以用到这个方法。

1
2
3
4
5
6
7
8
9
10
11

const timeout = (milliseconds) => new MyPromise((resolve,reject)=>{
setTimeout(reject, milliseconds, new Error('timeout'))
})

MyPromise.race([
someAsyncTask(),
timeout(30000), // 设置30秒后超时
]).then(()=>{
// ...
}).catch((error)=>{//...});

结语

当自己尝试完成Promise的实现后,很多之前的顾虑和疑惑顿时豁然开朗,还发现了一些之前不知道的Promise的使用技巧,但与此同时也暴露了自己平时偏重使用方式而忽略实现原理的问题。

古话说得好:学而不思则罔,自己平时在学习过程还是要多加强思考才行啊!

此处有本文的完整代码。