ES6 Promise

注:这是一篇学习笔记,记录自己在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
3
resolve({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
9
new 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
64
let 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
3
Promise.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
17
let 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
3
Promise.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
17
new 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
13
new 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
    11
    new 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
    29
    let 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
    17
    let 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
9
let 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
9
let 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
8
const 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
7
new 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
    9
    let 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
    3
    Promise.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
13
function 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
13
function 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
7
Promise.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]);

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要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的回调函数。