注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:
阮一峰ES6的书籍中Promise对象
我的ES6入门学习规划
Promise的含义
Promise是一个与异步任务的执行状态和执行结果保持同步的对象。
Promise正如它的字面含义一样,“保证”了以下几个特点:
- 它只有三个状态:pending, fulfilled, rejected,并且任何时候只能处于一个状态;
- 状态变化时,只能从pending变为fulfilled,或者从pending变为rejected;分别代表异步任务正常结束和非正常结束;
- 只有Promise关联的异步任务,才能更改Promise的状态;
- 一旦状态改变,意味着Promise包含的异步任务已经结束,且状态不会再发生改变;
- 当状态改变时,Promise对象会存储异步任务的执行结果;
- 当状态改变后,任何时候都能拿到Promise对象存储的执行结果;
Promise会“固化”异步任务,当异步任务结束后,任何时候都能通过Promise对象取得之前结束时的状态。
基本用法
Promise对象通过Promise的构造函数来实例化,新实例化的Promise实例状态默认为pending。1
new Promsie(function asyncTask(resolve, reject){/*异步代码*/})
构造函数接收一个函数参数asyncTask(这个名称是我加的,官方标准里面没有,实际使用也可以不加,只是为了讲明概念)。asyncTask在Promise实例化过程中立即执行,异步任务需编写在asyncTask内部。asyncTask接收两个函数参数,分别是resolve和reject,在asyncTask内部编写异步任务的时候,根据需要调用resolve和reject:resolve会把Promise实例状态从pending变为fulfilled;reject会把Promise实例状态从pending变为rejected。
resolve或reject都能接收参数,参数代表着异步任务的执行结果,并被传递到Promise内部进行保存。 resolve可根据需要传递任意类型的数据,reject也是如此,不过通常情况下reject的含义是异步任务非正常结束,所以reject传递的数据往往是Error类的实例。eg:1
2
3resolve({prop: 'value'});
reject(new Error('connect time out'));
实例化后的Promise实例,可通过then方法注册实例状态变为fulfilled时的回调函数;或通过catch方法注册实例状态变为rejected时的回调函数。then或catch方法注册的回调函数被调用时,Promise实例会把内部存储的异步任务执行结果传递给它们。eg:1
2
3
4
5
6
7
8
9new Promise(function asyncTask(resolve, reject){/*异步代码*/}).then(function(data){
//data就是异步任务正常结束的执行结果
});
// or
new Promise(function asyncTask(resolve, reject){/*异步代码*/}).catch(function(error){
//error就是异步任务非正常结束的执行结果
});
如果Promise实例状态已经发生改变,再次通过then或catch注册的回调函数,将立即执行,并能接收到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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64let promise = new Promise(function asyncTask(resolve, reject) {
// asyncTask在new Promise过程中立即执行
// 所以asynTask本身是同步执行的,只不过里面包含异步的代码
// 模拟编写异步任务
setTimeout(function(){
if(Math.random() * 10 > 5) {
// 模拟异步任务正常结束
resolve({msg: 'async task successfully finished!'});
// 下面的reject不会把promise实例状态改变为rejected
// 因为上一行resolve调用先执行,promise实例状态已经不会再变了
reject(new Error('this has no effects'));
} else {
// 模拟异步任务非正常结束
reject(new Error('unlucky random number!'));
// 下面的resolve不会把promise实例状态改变为fulfilled
// 因为上一行reject调用先执行,promise实例状态已经不会再变了
resolve({msg: 'this has no effects'});
}
},1000);// 1s后,异步任务会结束
});
console.log(promise);
//Promise {<pending>} 刚刚实例化的Promise状态默认是pending
//注册promise变为fulfilled时的回调函数
promise.then(function(data) {
// 通过data拿到异步任务正常结束时传递给promise的状态
console.log(data.msg);
// async task successfully finished!
console.log(promise); // Promise {<resolved>} resolved等同于fulfilled
});
//注册promise变为rejected时的回调函数
promise.catch(function(error) {
// 通过error拿到异步任务非正常结束时传递给promise的状态
// 另外一种含义是:通过error,拿到异步任务非正常结束的原因
console.error(error);
// Error: unlucky random number!
console.log(promise); // Promise {<rejected>}
});
setTimeout(function(){
// 2s后台再次注册回调,此时promise关联的异步任务早已结束
let start = Date.now();
promise.then(function(data) {
let end = Date.now();
console.log('get async task state',end - start, data.msg);
// get async task state 1 async task successfully finished!
// 中间的1表示then回调函数是在1ms之后调用的,而不是1000ms之后
});
promise.catch(function(error) {
let end = Date.now();
console.error('get async task state',end - start, error);
// get async task state 1 Error: unlucky random number!
// 中间的1表示then回调函数是在1ms之后调用的,而不是1000ms之后
});
},2000);
从这个例子,前面介绍的Promise特性都能得到印证。另外一个注意点就是resolve或reject调用只是单纯的函数调用,不是return,所以不会结束它们所在的函数执行,后面如果还有其它代码,依然会继续执行。
then实例方法
Promise对象可通过then这个实例方法,来注册实例状态变为fulfilled时回调函数。实际上then方法可以同时添加调用它的Promise实例被fulfilled或被rejected时的回调函数。 then方法的完整函数签名为:1
2
3Promise.prototype.then = function(fulfilledHandler, rejectedHandler) {
// [native code]
};
一般情况下,fulfilledHandler会在调用then的Promise实例被fulfilled时执行,而rejectedHandler会在调用then的Promise实例被rejected时执行;fulfilledHandler、rejectedHandler执行时会接收到调用then的Promise实例内部存储的异步任务结果。两个参数都是可省的,只传递一个,或者都不传,都是有效的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let promise = new Promise(function asyncTask(resolve, reject) {
// 模拟编写异步任务
setTimeout(function(){
if(Math.random() * 10 > 5) {
// 模拟异步任务正常结束
resolve({msg: 'async task successfully finished!'});
} else {
// 模拟异步任务非正常结束
reject(new Error('unlucky random number!'));
}
},1000);// 1s后,异步任务会结束
});
promise.then(function(data){
console.log(data.msg);
}, function(error) {
console.log(error);
});
注意在then回调中,data参数是promise内部resolve时传递过来的数据;error参数是promise内部reject时传递过来的数据。
catch方法实际上等同于1
2
3Promise.prototype.catch = function(rejectedHandler) {
return this.then(null, rejectedHandler);
};
所以只要重点学习then方法即可。
then方法返回一个全新的Promise对象。这个新的Promise对象同样可以调用then或catch方法再返回新的Promise对象,所以异步任务可以写成链式调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17new Promise(function asyncTask(resolve, reject) {
// 编写异步任务
}).then(function(data) {
// 异步回调
// 往后传递异步结果
return data;
}).then(function(data) {
// 第二个异步回调
// 对异步结果做一些额外处理,继续往后处理
return parseData(data);
}).then(function(data){
// 得到最终处理过的异步执行结果
}).catch(function(error){
// 处理非正常结束的异步任务
});
Promise的链式调用,可以给相同的异步任务,注册多个回调,前面then回调的返回值,会作为参数,传入下一个then回调。
也可以把多个异步任务写成链条,让它们串行执行:1
2
3
4
5
6
7
8
9
10
11
12
13new Promise(function asyncTask(resolve,reject){/*async code*/})
.then(function(data){
return new Promise(function asyncTask2(resolve,reject){/*async code2*/});
})//返回一个新的Promise实例,所以能链式地继续调用then方法
.then(function(data){
return new Promise(function asyncTask3(resolve,reject){/*async code3*/});
})//返回一个新的Promise实例,所以能链式地继续调用then方法
.then(function(data){
return new Promise(function asyncTask4(resolve,reject){/*async code4*/});
})//返回一个新的Promise实例,所以能链式地继续调用then方法
.then(function(data){
return new Promise(function asyncTask5(resolve,reject){/*async code5*/});
});
在这个情况下,由于then回调返回了新的Promise,代表开启了新的异步任务,所以后续的then回调都会等到前面then回调内的Promise状态改变的时候才会执行。
为了弄清这里面的作用规律,需要掌握then回调的调用对象、返回对象以及then回调的返回值三者之间的相互影响的机制。不需要关心整个链条,只需要搞懂一个环节上的关系即可。因为then方法的调用对象,一定是一个Promise对象,这个Promise对象可能是构造出来,也可能是其它的then调用返回的;then方法的返回对象,一定是一个Promise对象,它如果继续使用then,就会成为下一个环节的调用对象。所以,弄懂一个环节即可。
一般情况下then回调的调用对象、返回对象以及then回调的返回值三者之间的作用机制
- 调用对象被fulfilled,then的fulfilledHandler回调就会执行,并传入调用对象的状态;
- 调用对象被rejected,then的rejectedHandler回调就会执行,并传入调用对象的状态;
- fulfilledHandler 回调执行,它的返回值用来resolve掉then的返回对象;
- rejectedHandler 回调执行,它的返回值用来reject掉then的返回对象;
- 如果fulfilledHandler为null,返回对象会用调用对象相同的状态resovle掉;
- 如果rejectedHandler为null,返回对象会用调用对象相同的状态reject掉;
后面的两点,保证链式调用中,能够准确地传递异步任务的状态,不至于在中间某个环节丢掉。1
2
3
4
5
6
7
8
9
10
11new Promise(function asyncTask(resolve, reject) {
// 编写异步任务
}).then(function(data) {// 此处仅添加了fulfilledHandler回调 如果前面的Promise发生reject,此处回调不会执行,但是前面的reject状态会继续向后传递
return data;
}).then(function(data) {// 此处仅添加了fulfilledHandler回调 如果前面的Promise发生reject,此处回调不会执行,但是前面的reject状态会继续向后传递
return parseData(data);
}).catch(function(error){// 此处仅添加了rejectedHandler回调 如果前面的Promise没有发生reject,此处回调不会执行,但是前面的fulfilled状态会继续向后传递
// error
}).then(function(data){
// data
});
then的回调返回值是另外一个Promise实例
- 调用对象被fulfilled,then的fulfilledHandler回调就会执行,并传入调用对象的状态;
- 调用对象被rejected,then的rejectedHandler回调就会执行,并传入调用对象的状态;
- fulfilledHandler 或 rejectedHandler 回调执行时,由于它的返回值是另外一个Promise实例,所以此时then的返回对象不会同步then的调用对象的状态,而是转由回调返回的Promise实例来决定then的返回对象的状态;
- 不管是fulfilledHandler还是rejectedHandler返回的Promise实例,只要这个实例被resovle,那么then的返回对象就会用相同的状态被resolve;只要这个实例被reject,那么then的返回对象就会用相同的状态被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
29let p = new Promise(function(resolve){
setTimeout(function(){
resolve('first animation finish');
}, 1000);// 1s后这个异步任务结束
});
p.then(function(msg){
console.log(msg);// first animation finish
// p代表的异步任务结束时,再开启一个新的异步任务
return new Promise(function(resolve){
setTimeout(function(){
resolve('second animation finish');
}, 1000);// 1s后这个异步任务结束
});
})//then回调返回的Promise实例被fulfilled时,此处返回的Promise实例会被相同异步结果fulfilled
.then(function(msg){
console.log(msg);// second animation finish
// 前一个异步任务结束时,再开启一个新的异步任务
return new Promise(function(resolve){
setTimeout(function(){
resolve('third animation finish');
}, 1000);// 1s后这个异步任务结束
});
})//then回调返回的Promise实例被fulfilled时,此处返回的Promise实例会被相同异步结果fulfilled
.then(function(msg){
console.log(msg);// third animation finish
});
then的调用对象被resolve的时候传入了一个新的Promise实例
- 原先给调用对象加的then回调,将会等到这个新的Promise实例状态变化的时候才会执行;
剩下的规律跟前面两点一致。此种情况属于一种异步回调的转移,原先的then调用对象,被它resolve时传入的另外一个Promise实例给替换了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let start = Date.now();
let p = new Promise(function asyncTask(resolve,reject){
setTimeout(function(){
let inner = new Promise(function (resolve) {
setTimeout(function(){
resolve('inner promise fulfilled');
}, 1000);
});
resolve(inner);
}, 1000);
});
let p2 = p.then(function(msg){
console.log(Date.now() - start, msg);// 2017 "inner promise fulfilled"
// 2s以后p.then添加的回调才执行
});
rejectedHandler回调的额外说明
根据前面的说明,这个回调表示then的调用对象被rejected掉了,但是本身这个回调执行后,并不是把then的返回对象给reject掉,而是resolve掉:1
2
3
4
5
6
7
8
9let p = new Promise(function(resolve,reject){
setTimeout(function(){
reject(new Error('aa'));
}, 1000);// 1s后这个异步任务结束
});
let p2 = p.catch(function(error){
return error;
});
这个可能在理解的时候会出现分歧,所以单独说明。
如果想继续往后传递这个异步非正常结束的状态,直接返回error是不行的,可借助后面要记录的Promise.reject()这个静态方法:1
2
3
4
5
6
7
8
9let p = new Promise(function(resolve,reject){
setTimeout(function(){
reject(new Error('aa'));
}, 1000);// 1s后这个异步任务结束
});
let p2 = p.catch(function(error){
return Promise.reject(error); // 另一种写法: throw new Error(error);
});
Promise被reject的几种情况
- 大部分情况是通过主动调用Promise构造函数构造时调用reject回调参数
- Promise构造时出现异常,也会导致构造的实例被reject
- then方法的回调,不管是谁,抛出异常,也都会导致then返回的实例被reject
特殊地,在resolve之后,抛出异常不会导致Promise实例被reject,因为Promise实例只能改变一次状态:1
2
3
4
5
6
7
8const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
Promise会吞掉异常
- 不管是构造Promise的时候,还是then回调执行的时候,程序抛出错误,都会被Promise内部捕获,然后用来reject掉相关的Promise;
- Promise捕获到异常后,不会再向全局环境抛出异常,所以导致异常被吞并。
所以写Promise要记得写 rejectedHandler 回调。 为了使用更简洁,往往都是通过then来注册fulfilledHandler,通过catch方法来注册rejectedHandler。
then添加的回调是异步执行的
then注册的回调函数是在本次事件循环的末尾,下一轮事件循环之前执行;(这个要点关联到事件循环event-loop和micro task queue的知识,暂时也还没完全了解透,只掌握一些表面的东西)1
2
3
4
5
6
7new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
Promise.resolve静态方法
这个静态方法,可以将现有对象转换为Promise对象。它接收一个参数,返回一个Promise实例。根据参数的情况,有几种不同的处理方式:
- 参数是一个Promise实例:这个方法会原封不动地返回这个实例
参数是一个thenable对象:
1
2
3
4
5
6
7
8
9let obj = {
then: function(resolve, reject) {
// 异步
setTimeout(function(){
resolve('success');
},0);
}
};
// obj有一个then方法,then方法的签名跟Promise构造函数所需的参数完全一样Promise.resolve会利用thenable对象的then方法构造一个新的Promise实例来返回,好比:
1
new Promise(obj.then.bind(obj));
其它参数:这个方法会返回一个fulfilled的Promise实例,并且把这个参数存储为这个Promise实例内部的异步结果。
1
2
3Promise.resolve('hi').then(function(res){
console.log(res);//hi
});
所以直接对Promise.resolve()返回的fulfilled对象添加回调,就可以拿到之前Promise.resolve调用时传递的参数。
Promise.reject静态方法
这个方法跟Promise.resolve类似,接收任意参数,返回一个reject状态的Promise实例,并且接收的参数,会原封不动地存储为返回的Promise实例内部的异步结果。
finally实例方法
用过jquery的话,应该知道jquery的ajax实例有一个always方法,这个方法可以帮助我们做一些请求完成后的统一处理:1
2
3
4
5
6
7
8
9
10
11
12
13function sendData(btn, url, params) {
btn.disabled = true; //防止重复点击
return $.ajax({
url: url,
data: params
}).done(function(){
// success
}).fail(function(){
// fail
}).always(function(){
btn.disabled = false; //恢复按钮点击
});
}
这个always方法的使用需求就是为了做一些不管是done还是fail都需要做的事情,没有它,上面的代码就会变成:1
2
3
4
5
6
7
8
9
10
11
12
13function sendData(btn, url, params) {
btn.disabled = true; //防止重复点击
return $.ajax({
url: url,
data: params
}).done(function(){
// success
btn.disabled = false; //恢复按钮点击
}).fail(function(){
// fail
btn.disabled = false; //恢复按钮点击
});
}
ES里面,Promise也有类似的一个实例方法,18年才正式推出来,叫finally。这个方法与前面always方法的作用完全一样,它接收一个回调函数,这个回调函数无论在它之前then链发生fulfilled还是reject,只要到它这,这个回调函数就会执行;同时它也返回一个新的Promise实例,就像then方法一样,它会原封不动地把它回调前异步状态,传递到后面的then任务中。实现方式如下:1
2
3
4
5
6
7Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
从实现代码可以看到,finally的逻辑会原封不动地传递原来的异步结果,到后面的任务,它仅仅是作为一个无论结果如何都会执行callback的时机点而已。
Promise.all静态方法
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。1
const p = Promise.all([p1, p2, p3]);
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race静态方法
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。1
const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。