Promise
对象,ES6
新增的一个全新特性,虽然它出现很久了,but
我相信,有很多的小伙伴还是没有怎么用过,今天让我们来好好的学习一下它。
1、Promise 的含义
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作
)的结果。从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。
在日常开发中,经常需要用到ajax
请求数据,拿到数据后,再进行一些处理,完成一些复杂的交互效果。
假如你需要用ajax进行多次请求,而且,每次请求都依赖上一次请求返回的数据来作为参数,然后继续发出请求,你写成这样,场景还原:
1
2
3
4
5
6
7
8
9
10
11
12
$.ajax({
success:function(res1){
$.ajax({
success:function(res2){
$.ajax({
success:function(res3){
}
});
}
});
}
});
可能会有更多的嵌套,如此一层一层的执行,无疑是消耗了更多的等待时间,而且多个请求之间如果有先后关系的话,就会出现回调地狱,ES6想了办法整治这种情况,这时候Promise
诞生了。
所以我们知道了 Promise
是异步编程的一种解决方案,比传统的回调函数和事件更合理和强大。
Promise
对象有以下两个特点:
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来。- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调地狱。
有好就有坏,Promise
也有一些缺点。1、首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。2、其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。3、当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
2、Promise 的基本用法
接下来,我们就看看它的基本用法:
1
2
3
const promise = new Promise(function(resolve, reject) {
});
Promise
对象是全局对象,你也可以理解为一个类,创建Promise
实例的时候,要有那个new
关键字。Promise
构造函数接受一个函数作为参数,其中有两个参数:resolve
和reject
,两个函数均为方法。resolve
方法用于处理异步操作成功后业务(即从 pending
变为 resolved
)。reject
方法用于操作异步操作失败后的业务(即从 pending
变为 rejected
)。在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
上面也提到了,Promise
对象有三种状态:
- 1、pending:刚刚创建一个
Promise
实例的时候,表示初始状态; - 2、fulfilled:
resolve
方法调用的时候,表示操作成功; - 3、rejected:
reject
方法调用的时候,表示操作失败; 下面代码创造了一个Promise
实例。1 2 3 4 5 6 7 8 9 10 11
const promise = new Promise(function(resolve, reject) { //实例化后状态:pending if (/* 异步操作成功 */){ resolve(value); // resolve方法调用,状态为:fulfilled } else { reject(error); // reject方法调用,状态为:rejected } });
初始化实例后,对象的状态就变成了
pending
;当resolve
方法被调用的时候,状态就会由pending
变成了成功fulfilled
;当reject
方法被调用的时候,状态就会由pending
变成失败rejected
。
Promise
实例生成以后,可以用then()
方法分别指定resolved
状态和rejected
状态的回调函数,用于绑定处理操作后的处理程序。
看以下操作:
1
2
3
4
5
promise.then(function(value) {
// 操作成功的处理程序
}, function(error) {
// 操作失败的处理程序
});
then()
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise
对象传出的值作为参数。
说简单点就是参数是两个函数,第一个用于处理操作成功后的业务,第二个用于处理操作异常后的业务。
举一个Promise对象的简单例子。
1
2
3
4
5
6
7
8
9
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'hello world');
});
}
timeout(100).then((value) => {
console.log(value); // hello world
});
上面代码中,timeout()
方法返回一个Promise
实例,表示一段时间以后才会发生的结果。过了指定的时间(ms
参数)以后,Promise
实例的状态变为resolved
,就会触发then()
方法绑定的回调函数。
Promise
新建后就会立即执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
Promise
新建后立即执行,所以首先输出的是Promise
。然后,then()
方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved
最后输出。
对于操作异常的程序,Promise
专门提供了一个实例方法来处理:catch()
方法。
catch()
只接受一个参数,用于处理操作异常后的业务。
1
2
3
4
5
6
getJSON('/posts.json').then(function(posts) {
// 处理成功
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise
使用链式调用,是因为then方法
和catch方法
调用后,都会返回promise对象。
上面代码中,getJSON()
方法返回一个 Promise
对象,如果该对象状态变为resolved
,则会调用then()
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch()
方法指定的回调函数,处理这个错误。另外,then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
1
2
3
4
5
6
7
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代码中,promise
抛出一个错误,就被catch方法
指定的回调函数捕获。
如果 Promise
状态已经变成resolved
,再抛出错误是无效的。
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
在resolve
语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise
的状态一旦改变,就永久保持该状态,不会再变了。
因为 Promise.prototype.then
和 Promise.prototype.catch
方法返回promise
对象, 所以它们可以被链式调用
注意
: 如果一个promise
对象处在fulfilled
或rejected
状态而不是pending
状态,那么它也可以被称为settled
状态。你可能也会听到一个术语resolved
,它表示promise
对象处于settled
状态。
举了这么多栗子,要是没看懂,让我们串一串吧,梳理一下上面提到的。(认真的看注释)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 首先用 new 关键字创建一个 `Promise` 实例
const promise = new Promise(function(resolve, reject){
// 假设 state 的值为 true
let state = true;
if( state ){
// 调用操作成功方法
resolve('操作成功');
//状态:从pending 到 fulfilled
}else{
// 调用操作异常方法
reject('操作失败');
//状态:从pending 到 rejected
}
});
promise.then(function (res) {
//操作成功的处理程序
console.log(res)
}).catch(function (error) {
//操作失败的处理程序
console.log(error)
})
// 控制台输出:操作成功
上面示例介绍了从 创建实例,状态转换,then方法和catch方法的使用。
如果多个操作之间层层依赖,我们用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
const promise = new Promise(function(resolve, reject){
if( true ){
// 调用操作成功方法
resolve('操作成功');
//状态:从pending 到 fulfilled
}else{
// 调用操作异常方法
reject('操作失败');
//状态:从pending 到 rejected
}
});
promise.then(a)
.then(b)
.then(c)
.catch(requestError);
function a() {
console.log('请求A成功');
return '请求B,下一个是谁';
}
function b(res) {
console.log('上一步的结果:'+ res);
console.log('请求B成功');
return '请求C,下一个是谁';
}
function c(res) {
console.log('上一步的结果:'+ res);
console.log('请求C成功');
}
function requestError() {
console.log('请求失败');
}
如图所示:
上面的代码,先是创建一个实例,还声明了4个函数,其中三个是分别代表着
请求A,请求B,请求C
;有了then
方法,三个请求操作再也不用层层嵌套了。我们使用then
方法,按照调用顺序,很直观地完成了三个操作的绑定,并且,如果请求B
依赖于请求A
的结果,那么,可以在请求A
的程序用使用return
语句把需要的数据作为参数,传递给下一个请求,示例中我们就是使用return
实现传递参数给下一步操作的。
为了更直观的看到示例所展示的情况,在下做了一张图:
再举个Promise 中微任务顺序的栗子1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p = new Promise( (resolve, reject) => {
setTimeout( () => {
console.log('1');
}, 3000);
resolve(1);
}).then( () => { // 描述:.then() 1-1
Promise.resolve().then( () => { // 描述:.then() 2-1
Promise.resolve().then( () => { // 描述:.then() 3-1
console.log('2');
})
})
}).then( () => { // 描述:.then() 1-2
console.log('3');
})
// 3
// 2
// 1 (3秒之后执行打印的值)
如上示例解释:
- 1.先执行
new Promise
第一层的代码,遇到setTimeout
,将其推入宏任务队列中(此时未执行,排在当前script代码块的宏任务之后执行),然后遇到了resolve
,执行Promise
后面的代码。- 2.遇到
.then 1-1
,推入微任务队列里(只是推入,并未执行,所以.then 1-2
的执行时机还没有到),这个时候发现没有其他的操作需要处理(比如推其他的微任务到队列里),那么就执行当前微任务队列里的函数,也就是执行.then 1-1
的回调函数。- 3.执行
.then 1-1
的回调函数的时候,发现了里面有一个完成态的Promise
对象,不用管继续走,遇到了.then 2-1
,推入微任务队列(只是推入,并未执行),此时.then 1-1
回调执行完毕(没有return值,相当于return了一个undefined),然后Promise
得以继续往下执行,遇到了.then 1-2
,继续推入微任务队列(依然没执行),这时发现没有其他操作,开始顺位执行当前微任务队列里的函数(此时微任务队列里存放了.then 2-1
和.then 1-2
的回调函数),执行.then 2-1
的回调函数时,又遇到了一个完成态的Promise
,不用管继续走,遇到了.then 3-1
,将其推入微任务队列(未执行),然后执行.then1-2
的回调,打印3
,此时已经没有了其他的操作,所以继续执行微任务队列里剩余的函数,即.then 3-1
的回调函数,打印2
。- 4.至此,微任务队列已经执行完毕,开始执行宏任务队列中的下一个宏任务,打印
1
。
再举个Promise 中微任务顺序的栗子2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p2 = new Promise( (resolve, reject) => {
setTimeout( () => {
console.log('1');
}, 3000)
resolve(1);
}).then( () => { // 描述:.then() 1-1
Promise.resolve().then( () => { // 描述:.then() 2-1
console.log('2');
}).then( () => { // 描述:.then() 1-2
console.log('3');
})
})
// 2
// 3
// 1 (3秒之后执行打印的值)
如上示例解释:
- 1.如同栗子1。
- 2.如同栗子2。
- 3.执行
.then 1-1
的回调函数的时候,发现了里面有一个完成态的Promise
对象,不用管继续走,遇到了.then 2-1
,推入微任务队列(只是推入,并未执行),此时.then 1-1
回调执行完毕(没有return
值,相当于return
了一个undefined
),然后Promise
得以继续往下执行,遇到了.then 1-2
,继续推入微任务队列(依然没执行),这时发现没有其他操作,开始顺位执行当前微任务队列里的函数(此时微任务队列里存放了.then 2-1
和.then 1-2
的回调函数),执行.then 2-1
的回调函数,打印2
,然后执行.then1-2
的回调,打印3
。- 4.至此,微任务队列已经执行完毕,开始执行宏任务队列中的下一个宏任务,打印
1
。
除了提供了实例方法以外,Promise还提供了一些类方法,也就是不用创建实例,也可以调用的方法,下面列举几个栗子:
Promise.all(iterable)
方法返回一个 Promise
实例,此实例在 iterable
参数内所有的 promise
都“完成(resolved
)”或参数中不包含 promise
时回调完成(resolve
);如果参数中 promise
有一个失败(rejected
),此实例回调失败(reject)
,失败原因的是第一个失败 promise
的结果。
1
2
3
4
5
6
7
8
9
10
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 3000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(values => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
解析:
因为实例
promise3
还没有进入成功fulfilled
状态;等到了3000
毫秒以后,实例promise3
也进入了成功fulfilled
状态,Promise.all( )
才会进入then
方法,然后在控制台输出:[3, 42, “foo”]
应用场景:我们执行某个操作,这个操作需要得到需要多个接口请求回来的数据来支持,但是这些接口请求之前互不依赖,不需要层层嵌套。这种情况下就适合使用Promise.all( )
方法,因为它会得到所有接口都请求成功了,才会进行操作。
注意:
如果传入的 promise
中有一个失败(rejected
),Promise.all
异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise
是否完成。
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。这避免了同样的语句需要在then()
和catch()
中各写一次的情况。
1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
下面是一个例子,服务器使用 Promise
处理请求,然后使用finally
方法关掉服务器。
1
2
3
4
5
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
如果你想在 promise
执行完毕后无论其结果怎样都做一些处理或清理时,finally()
方法可能是有用的。
Promise.race()
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。
1
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise
实例的返回值,就传递给p的回调函数。
Promise.race
方法的参数与Promise.all
方法一样,如果不是 Promise
实例,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise
实例,再进一步处理。
1
2
3
4
5
6
7
8
9
10
const p = Promise.race([
fetch('index.php'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch
方法无法返回结果,变量p
的状态就会变为rejected
,从而触发catch
方法指定的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let promise1 = new Promise(function(resolve){
setTimeout(function () {
resolve('promise1实例成功');
},4000);
});
let promise2 = new Promise(function(resolve,reject){
setTimeout(function () {
reject('promise2实例失败');
},2000);
});
Promise.race([promise1, promise2]).then(function(result){
console.log(result);
}).catch(function(error){
console.log(error);
});
// expected output: promise2实例失败
由于promise2
实例中2000
毫秒之后就执行reject方法,早于实例promise1
的5000
毫秒,所以最后输出的是:promise2实例失败。
有时需要将现有对象转为 Promise 对象,Promise.resolve
方法就起到这个作用。
1
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代码将 jQuery
生成的deferred
对象,转为一个新的 Promise
对象。
Promise.resolve
等价于下面的写法。
1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve(value)
方法返回一个以给定值解析后的Promise
对象。如果该值为promise
,返回这个promise
;如果这个值是thenable
(即带有“then” 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise
将以此值完成。此函数将类promise
对象的多层嵌套展平。
警告
:不要在解析为自身的thenable 上调用Promise.resolve。这将导致无限递归,因为它试图展平无限嵌套的promise。一个例子是将它与Angular中的异步管道一起使用。
使用静态Promise.resolve方法
1
2
3
4
5
Promise.resolve("Success").then(function(value) {
console.log(value); // "Success"
}, function(value) {
// 不会被调用
});
resolve一个数组
1
2
3
4
var p = Promise.resolve([1,2,3]);
p.then(function(v) {
console.log(v[0]); // 1
});
Resolve另一个promise
1
2
3
4
5
6
7
8
9
10
11
12
var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));
/*
* 打印顺序如下,这里有一个同步异步先后执行的区别
* original === cast ? true
* value: 33
*/
Promise.reject(reason)
方法返回一个带有拒绝原因reason
参数的Promise
对象。
1
2
3
4
5
6
7
8
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
上面代码生成一个 Promise
对象的实例 p
,状态为rejected
,回调函数会立即执行。
注意
,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
1
2
3
4
5
6
7
8
9
10
11
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
举一个promise 应用的栗子:
我们可以将图片的加载写成一个Promise
,一旦加载完成,Promise
的状态就发生变化。
1
2
3
4
5
6
7
8
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
Generator 函数与 Promise 的结合
使用 Generator函数 管理流程,遇到异步操作的时候,通常返回一个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
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
上面代码的 Generator
函数g之中,有一个异步操作getFoo
,它返回的就是一个Promise
对象。函数run
用来处理这个Promise
对象,并调用下一个next
方法。
我目前所写的项目大多数都是Generator函数 与 Promise 的结合。
这个篇幅有点藏,如果你没有收藏可以收藏,以后慢慢的观看。
一下是我的公众号,关注我,会让你有意想不到的收获~