一、概述

1. Promise解决了回调处理异步问题时存在的两个弊端:回调地狱、难以处理错误。

2. Promise实例有三个状态:pending、fulfilled、rejected。

Promise实例有两个过程:pending->fulfilled、pending->rejected

3. Promise的局限性

(1)状态从pending变为fulfilled、或从pending变为rejected,一旦状态改变就不会再变。当promise实例被创建时,内部的代码就会立即被执行,而且无法从外部停止。比如无法取消超时或消耗性能的异步调用,容易导致资源的浪费。

(2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

(3)Promise处理问题都是一次性的,因为一个Promise实例只能resolve或reject一次,所以面对某些需要持续响应的场景时就会变得力不从心。比如上传文件获取进度时,默认采用的就是事件监听的方式实现。

下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const https = require('https');

function httpPromise(url){
return new Promise((resolve,reject) => {
https.get(url, (res) => {
resolve(data);
}).on("error", (err) => {
reject(error);
});
})
}

httpPromise().then((data) => {
console.log(data)
}).catch((error) => {
console.log(error)
})

可以看到,Promise会接收一个执行器,在这个执行器里,需要把目标异步任务给放进去。在Promise实例创建后,执行器里的逻辑会立即执行,在执行过程中,根据异步返回的结果,决定如何使用resolve或reject来改变Promise实例的状态。

当用resolve切换到了成功态后,Promise的逻辑就会走到then中传入的方法里去,用reject切换到失败态后,Promise的逻辑就会走到catch传入的方法中。

这样的逻辑,本质上和回调函数中的成功回调和失败回调没有差异。但这种写法大大地提高了代码的质量。当我们进行大量的异步链式调用时,回调地狱不复存在了。取而代之的是层级简单、赏心悦目的Promise调用链。

二、Promise的创建

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。

1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) => {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

一般情况下,我们会用new Promise()来创建Promise对象。除此之外,还可以使用Promise.resolve和Promise.reject这两个方法来创建:

1. Promise.resolve

Promise.resolve(value)的返回值是一个promise对象,我们可以对返回值进行.then调用,如下代码:

1
2
3
Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});

resolve(11)会让promise对象进入确定(resolve状态),并将参数11传递给后面then中指定的onFulfilled函数。

2. Promise.reject

1
2
3
4
5
6
Promise.reject(new Error("我错了!"));

// 上面是以下代码的简化形式
new Promise((resolve, reject) => {
reject(new Error("我错了!"));
});

三、Promise的作用

在开发中可能会碰到这样的需求:使用ajax发送A请求,成功后拿到数据,需要把数据传给B请求,那么需要这样编写代码:

1
2
3
4
5
6
7
8
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})

这段代码之所以看上去很乱,归结其原因有两点:

  • 第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
  • 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。

既然原因分析出来了,那么问题的解决思路就很清晰了:

  • 消灭嵌套调用;
  • 合并多个任务的错误处理。

这么说可能有点抽象,不过 Promise 解决了这两个问题。接下来就看看 Promise 是怎么消灭嵌套调用和合并多个任务的错误处理的。

Promise出现之后,代码可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})

通过引入 Promise,上面这段代码看起来就非常线性了,也非常符合人的直觉。

1
2
3
4
5
6
7
8
9
readFilePromise('1.json').then(data => {
return readFilePromise('2.json');
}).then(data => {
return readFilePromise('3.json');
}).then(data => {
return readFilePromise('4.json');
}).catch(err => {
// xxx
})

Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。

四、Promise的方法

Promise常用的方法:then()、catch()、finally()、all()、race()、allSettled()、any()

1. Promise构造函数:Promise(excutor){}

excutor函数:同步执行 (resolve, reject)=>{}

resolve函数:内部定义成功时我们调用的函数 value=>{}

reject函数:内部定义失败时我们调用的函数 reason=>{}

说明:excutor会在Promise内部立即同步回调,异步操作在执行器中执行

2. Promise.prototype.then():(onResolved, onRejected)=>{}

onResolved函数:成功的回调函数 (value)=>{}

onRejected函数:失败的回调函数 (reason)=>{}

说明:指定用于得到成功value的成功回调和用于得到失败reason的失败回调,返回一个新的promise对象

then会接收两个参数(函数),第一个参数会在执行resolve之后触发(还能传递参数),第二个参数会在执行reject之后触发(其实也可以传递参数,和resolve传递参数一样),但是对于Promise中的异常处理,我们建议用catch方法,而不是then的第二个参数。

3. Promise.prototype.catch():(onRejected)=>{}

onRejected函数:失败的回调函数 (reason)=>{}

说明:then()的语法糖,相当于:then(undefined, onRejected)

4. Promise.resolve():(value)=>{}

value: 成功的数据或promise对象

说明:返回一个成功/失败的promise对象

5. Promise.reject():(reason)=>{}

reason:失败的原因

说明:返回一个失败的promise对象

6. Promise.all()

读取两个文件data1.json和data2.json,现在我需要一起读取这两个文件,等待它们全部都被读取完,再做下一步的操作。此时需要用到Promise.all。

当数组中的所有promise的状态都达到resolved时,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。两个promise都为resolved时,all()就成功,否则就失败。

1
2
3
4
5
6
// Promise.all 接收一个包含多个 promise 对象的数组
Promise.all([result1, result2]).then(datas => {
// 接收到的 datas 是一个数组,依次包含了多个 promise 返回的内容
console.log(datas[0])
console.log(datas[1])
})

7. Promise.race()

读取两个文件data1.json和data2.json,现在我需要一起读取这两个文件,但是只要有一个已经读取了,就可以进行下一步的操作。此时需要用到Promise.race

如果第一个promise状态是resolved,那race()就直接返回resolved;反之,如果第一个promise变成rejected,那race()就会变成rejected。所有,race()结果取决于响应的第一个promise的状态。

1
2
3
4
5
// Promise.race 接收一个包含多个 promise 对象的数组
Promise.race([result1, result2]).then(data => {
// data 即最先执行完成的 promise 的返回值
console.log(data)
})

8. Promise.allSettled()

这是ES11新增的特性。

Promise.allSettled的语法及参数跟all类似,不同之处在于通过Promise.allSettled我们可以拿到每个Promise的状态,而不管其是否处理成功。

1
2
3
4
5
6
7
8
9
10
11
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// 返回结果:
// [
// { status: 'fulfilled', value: 2 },
// { status: 'rejected', reason: -1 }
// ]

9. ES12新增的 Promise.any()

只要有一个成功就返回fulfilled,如果所有参数Promise实例都为rejected状态,那么any()最终结果就是rejected。

1
2
3
4
5
6
7
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// 返回结果:2

10. ES9新增 Promise.prototype.finally()

五、Promise的异常处理

错误处理是所有编程范式都必须要考虑的问题,在使用JavaScript进行异步编程时也不例外。如果我们不做特殊处理,会怎样呢?来看下面的代码,先定义一个会失败的方法。

1
2
3
4
5
let fail = () => {
setTimeout(() => {
throw new Error("fail");
}, 1000);
};

调用:

1
2
3
4
5
6
7
console.log(1);
try {
fail();
} catch (e) {
console.log("captured");
}
console.log(2);

可以看到打印出了1和2,并在1秒后,获得一个“Uncaught Error”的错误打印,注意观测这个错误的堆栈:

1
2
Uncaught Error: fail
at <anonymous>:3:9

可以看到,其中的 setTimeout (async) 这样的字样,表示着这是一个异步调用抛出的堆栈。但是,“captured”这样的字样也并未打印,因为母方法 fail() 本身的原始顺序执行并没有失败,这个异常的抛出是在回调行为里发生的。 从上面的例子可以看出,对于异步编程来说,使用try…catch是无法捕获错误的。我们需要使用一种更好的机制来捕获并处理可能发生的异常。

Promise 除了支持 resolve 回调以外,还支持 reject 回调,前者用于表示异步调用顺利结束,而后者则表示有异常发生,中断调用链并将异常抛出:

1
2
3
4
5
6
const exe = (flag) => () => new Promise((resolve, reject) => {
console.log(flag);
setTimeout(() => {
flag ? resolve("yes") : reject("no");
}, 1000);
});

上面的代码中,flag 参数用来控制流程是顺利执行还是发生错误。在错误发生的时候,no 字符串会被传递给 reject 函数,进一步传递给调用链:

1
2
3
Promise.resolve()
.then(exe(false))
.then(exe(true));

上面的调用链,在执行的时候,第二行就传入了参数 false,它就已经失败了,异常抛出了,因此第三行的 exe 实际没有得到执行,执行结果如下:

1
2
false
Uncaught (in promise) no

这就说明,通过这种方式,调用链被中断了,下一个正常逻辑 exe(true) 没有被执行。 但是,有时候需要捕获错误,而继续执行后面的逻辑,该怎样做?这种情况下就要在调用链中使用 catch 了:

1
2
3
4
Promise.resolve()
.then(exe(false))
.catch((info) => { console.log(info); })
.then(exe(true));

这种方式下,异常信息被捕获并打印,而调用链的下一步,也就是第四行的 exe(true) 可以继续被执行。将看到这样的输出:

1
2
3
false
no
true

六、Promise 原理

Promise只是对于异步操作代码可读性的一种变化,它并没有改变 JS 异步执行的本质,也没有改变 JS 中存在callback的现象。

值得注意的是,Promise 是用来管理异步编程的,它本身不是异步的,new Promise的时候会立即把executor函数执行,只不过我们一般会在executor函数中处理一个异步操作。如下例子:

1
2
3
4
5
6
7
let p1 = new Promise(()=>{
setTimeout(()=>{
console.log(1)
},1000)
console.log(2)
})
console.log(3) // 2 3 1

Promise 采用了回调函数延迟绑定技术,在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。这具体是啥意思呢?我们先来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let p1 = new Promise((resolve,reject)=>{
console.log(1);
resolve('浪里行舟')
console.log(2)
})

// then:设置成功或者失败后处理的方法
p1.then(result=>{
//p1延迟绑定回调函数
console.log('成功 '+result)
}, reason=>{
console.log('失败 '+reason)
})
console.log(3)

// 1
// 2
// 3
// 成功 浪里行舟

new Promise的时候先执行executor函数,打印出 1、2,Promise在执行resolve时,触发微任务(其实就是setTimeout(()=>{}, 0)),还是继续往下执行同步任务,执行p1.then时,存储起来两个函数(此时这两个函数还没有执行),然后打印出3,此时同步任务执行完成,最后执行刚刚那个微任务,从而执行.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
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
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
// 保存初始化状态
var self = this;

// 初始化状态
this.state = PENDING;

// 用于保存 resolve 或者 rejected 传入的值
this.value = null;

// 用于保存 resolve 的回调函数
this.resolvedCallbacks = [];

// 用于保存 reject 的回调函数
this.rejectedCallbacks = [];

// 状态转变为 resolved 方法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}

// 保证代码的执行顺序为本轮事件循环的末尾(放到微任务中)
setTimeout(() => {
// 只有状态为 pending 时才能转变,
if (self.state === PENDING) {
// 修改状态
self.state = RESOLVED;

// 设置传入的值
self.value = value;

// 执行回调函数
self.resolvedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}

// 状态转变为 rejected 方法
function reject(value) {
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变
if (self.state === PENDING) {
// 修改状态
self.state = REJECTED;

// 设置传入的值
self.value = value;

// 执行回调函数
self.rejectedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}

// 将两个方法传入函数执行
try {
fn(resolve, reject);
} catch (e) {
// 遇到错误时,捕获错误,执行 reject 函数
reject(e); // 决定了所有的错误最终都会冒泡到catch()执行
}
}

MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {
return value;
};

onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {
throw error;
};

// 如果是等待状态,则将函数加入对应列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}

// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) {
onResolved(this.value);
}

if (this.state === REJECTED) {
onRejected(this.value);
}
};

八、Promise 库 Q.js 的使用