在日常的工作中经常会遇到需要请求多次异步的情况,但是由于异步返回时间的不确定性,因此有时候会给我们带来很多的问题和麻烦。在我们被异步嵌套的头昏脑胀的时候,我们是多么希望JS能够像JAVA一样是同步执行的。带着这样解决问题的信念,笔者学习了一下Promise,发现还挺好用的,写一下笔者的使用心得。

  Promise在英文中的解释就是承诺,在爱情中时常用来表示比较罗曼蒂克的憧憬,但是在JS中没有这么浪漫,只是单纯地表示无论操作成功或者失败,一定会给出一个“反馈”。
  就好比媳妇喊你去街上打酱油,最后只有两种可能性,一种可能性是你成功的打到了酱油,回来给她了;另一种可能性就是酱油卖光或者其他原因,然后你没有打到酱油,但是你还是会跑去跟你媳妇汇报,然后你媳妇就会在心里默默的想下面这张图:

Promise

ES6中的Promise

  咳咳,有点扯远了,那么首先让我们用console来揭开Promise的真正面目吧。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

什么是Promise

show-promise

  通过控制台打印出来,我们看到原来Promise其实是一个构造函数,它的构造函数上有resolve和reject等其他几个方法,原型上也有then、catch等方法。既然是构造函数,那么肯定是能够通过new来创建一个对象的。

Promise的特点

  介绍了Promise,那么来说一下它的两个特点吧。

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。

  说了这么多,相信笔者也迫不及待了,先来构建一个Promise看看吧。

构建一个Promise

1
2
3
4
5
6
7
8
9
10
11
function getPromise1() {
var p = new Promise((resolve, reject) => {
// 这里放一些异步操作
setTimeout(() => {
console.log('异步1执行完成');
resolve('异步1返回数据');
}, 1000);
});
return p;
}
getPromise1();

  看到这里读者肯定觉得跟以前比没有很大的变化,而且有很多的困惑,比如:

  1. 为什么Promise外层要包一个函数把它return出去,直接创建对象不就行了么。
  2. Promise的构造函数传入一个函数,这个函数接受两个参数,这两个参数有什么用。

  运行上面的代码,控制台就会打印”异步1执行完成”。我们只是构造了一个Promise的对象,并没有调用它里面的方法,就已经执行了,所以这就是为什么要将Promise放到函数中调用获取的原因。同时,使用一个函数返回对象更加符合函数式编程的思想。这边的resolve方法的作用就是将Promise对象从Pending状态置为Resolved状态。

异步数据处理

1
2
3
4
5
getPromise1()
.then((data) => {
// 一些业务逻辑处理
console.log(data);
});

  getPromise1方法获取到的就是我们上面返回的Promise对象,直接调用then方法,表示在异步结束后调用此方法。它接受一个参数,是一个函数,这个函数默认会传入一个参数,这个参数就是我们在Promise构造函数中所调用的resolve所传入的异步数据。
  感情绕了一大圈,其实就是把原有在异步完成后的业务逻辑单独抽离出一个方法么?其实Promise还能做更多。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

我有多个异步

  如果这个时候来了一个需求,这个异步的数据不够,还需要发另外一个异步。如果按照以前的逻辑肯定是在then回调方法的继续来发异步,然后就陷入了恶(e)性(xin)的嵌套,如果业务逻辑很复杂,而且还需要发异步,那么这个函数里面代码也会越来越庞大,后期维护起来会非常的麻烦。但是Promise的出现拯救了这一切。

Super-Promise

  Promise的优势在于能够进行链式的调用,将原来嵌套调用转为线性调用。在then方法中继续返回一个新的Promise对象,然后能够继续调用then方法。

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
getPromise1()
.then((data) => {
console.log(data);
return getPromise2();
})
.then((data) => {
console.log(data);
return getPromise3();
})
.then((data) => {
console.log(data);
});

function getPromise2() {
var p = new Promise((resolve, reject) => {
// 这里放一些异步操作
setTimeout(() => {
console.log('异步2执行完成');
resolve('异步2返回数据');
}, 2000);
});
return p;
}
function getPromise3() {
var p = new Promise((resolve, reject) => {
// 这里放一些异步操作
setTimeout(() => {
console.log('异步3执行完成');
resolve('异步3返回数据');
}, 3000);
});
return p;
}

  我们会看到每隔一秒、两秒、三秒就会打印一组“执行完成n和返回数据n”。在then方法中也可以不返回一个Promise对象,直接返回数据。将上面的代码如下改写:

1
2
3
4
5
6
7
8
9
10
11
12
getPromise1()
.then((data) => {
console.log(data);
return data;
})
.then((data) => {
console.log(data);
return data;
})
.then((data) => {
console.log(data);
});

  最后可以看到,一秒之后打印了一次执行完成1和三次返回数据1。这样的then方法没有什么意义。

reject方法

  细心的读者可能发现了,在Promise的构造方法中还有一个reject方法还没有被用到。既然是异步,那么肯定有成功也有失败的时候,reject方法的作用是将Promise对象的状态置为Rejected状态,在then方法中执行失败情况的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getPromise4() {
var p = new Promise((resolve, reject) => {
setTimeout(() => {
var number = Math.round(Math.random()*10);
if(number %2 == 0) {
resolve('成功数据');
} else {
reject('失败数据')
}
}, 100);
});
return p;
}
getPromise4()
.then(
(data) => {
console.log('成功回调:' + data);
},
(data) => {
console.log('失败回调:' + data);
}
)

  我们首先获取一个随机数,判断这个随机数是否是偶数,如果是偶数的话就进入成功的回调方法;如果失败了就进入失败的回调方法。在then方法中我们发现多传入了一个方法,第一个方法还是成功情况的回调,第二个方法就是失败情况的回调,可以不传,不传的话就默认只有成功的回调。
  但是需要注意的是,如果不传失败的回调函数,但是同时你还调用了reject方法,这时候Promise内部会报错。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

进阶方法

  在Promise对象上还有一些其他方法。

catch方法

  在Promise的原型上还有一个catch方法,我们知道try/catch方法是用来捕捉异常的,Promise中的catch方法也可以做同样的事情。将上面的方法进行如下改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getPromise4()
.then(
(data) => {
console.log('成功回调:' + data);
console.log(temp);
},
(data) => {
console.log('失败回调:' + data);
console.log(temp);
}
)
.catch((err)=>{
console.log('捕获异常',err);
})

  在执行Promise的回调方法时可能会进入到第一个成功的回调函数中也可能会进入到第二个失败的回调函数中,如果回调函数中有抛出异常,并不会因为这个异常而卡死程序,就会进入到catch方法中捕捉到异常。最终运行的效果有以下两种可能性:

result_catch

all方法

  Promise中还有一个all方法,all是全部的意思,因此我们能猜测,就是等所有的异步都执行完毕。all方法让Promise有并行执行异步的能力,等所有并行异步执行完成后才执行回调。

1
2
3
4
Promise.all([getPromise1(),getPromise2(),getPromise3()])
.then((results) => {
console.log('所有异步结束', results)
});

  可以看到all接受一个Promise的数组,数组中是三个Promise对象。在第一个和第二个异步执行完成后都没有进入then方法,而是等最后一个最慢的异步执行完了才进入then方法。最终,所有异步操作的结果都通过then方法的参数以数组的形式传递进来。最终运行效果如下:

result_all

  但是问题来了,如果多个异步中有一个异步执行失败了呢?如果这个异步失败是通过reject方法抛出的,那么此时其他Pending中的异步还是会继续去运行,但是这个时候就会提前进入then方法的第二个参数函数中去,这个函数的默认参数也只是这个失败异步reject所发送的数据(不一定还是数组),等其他异步执行完成也不会再去执行then方法了。

race方法

  all方法执行的效果是等大家都结束了再运行,但是race是赛跑、竞争的意思,那么就很明显了,就是谁跑的快就有肉吃。将上面的代码进行改写:

1
2
3
4
Promise.race([getPromise1(),getPromise2(),getPromise3()])
.then((results) => {
console.log('results', results)
});

  这三个异步同样是并行执行的,但是race的then方法优先执行先完成的异步。第一个异步getPromise1先执行完,因此先进入then方法。此时getPromise2和getPromise3还没有执行完,还会继续执行,但是不会再去执行then方法了。最后执行结果如下:

result_race

总结

  本文中介绍的所有异步操作均以setTimeout作为例子,如果有不正确的地方欢迎指正。

本文地址: http://xieyufei.com/2017/01/21/FrontEnd-Promise.html