Promise 是什么

Promise 是抽象异步处理对象以及对其进行各种操作的组件。 通俗点讲,Promise能解决由于回调嵌套带来的流程控制与可读性问题。
promise 已经是成为我们解决回调炼狱的常用方案,而且已经得到官方标准支持,如果你刚刚开始使用Promise,本文将帮助你了解几个常见的Promise的使用场景。

Promise 的历史

早在1976年就有人提出Promise 的概念。之后的计算机语言发展中,很多语言都提供了与 Promise 相关的特性。而对于Javascript语言来说,最早让大家广泛接触的 Promise 相关的库是由 jQuery.Deferred() 对象实现的。随着 Promise/A+ 标准规定了一系列 API,实现该标准的库如雨后春笋版涌现了出来,在最新的ECMAScript 2015中已经提供了Promise的内置对象,成为了基础库。

一些Promise的使用场景

1.原生API函数的Promise化

大部分原生的API函数并不支持Promise,还是基于回调来使用的,所以需要把一些方法改为返回一个 Promise 对象,这个过程被称为函数的 Promise 化。
下面一个例子将对定时器setTimeout 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
function timer(fn,time){
return function(){
return new Promise( (resolve,reject)=>{
setTimeout(function(){
fn();
resolve();
},time);
});
}
}
Promise.resolve()
.then(
timer(function () {
console.log('1')
}, 1000)
)
.then(() => {
console.log('2');
});
/*
输出结果
1
2
*/

Promise化本质上都属于一种Curry化。Curry化是指,将需要传递多参数的函数生成一个新的函数,如上代码先通过执行 timer得到一个新的函数,该函数会返回一个Promise,这样就完成了Promise化。将一些基础的函数进行Promise化,可以
大大减少不必要的代码。
下面的代码,将会体现这种优势:

1
2
3
4
5
6
7
8
9
10
11
12
var promise_timer = timer(function () {
console.log('1')
}, 1000)
function promise_timer2(){
return new Promise( (resolve,reject)=>{
setTimeout(function(){
console.log('1');
resolve();
},1000);
});
}

变量promise_timer赋予的函数,与函数promise_timer2是等价的。 可以看出 setTimeout Promise 化之后,代码程序可读性更强,代码量也变少了。

2.Promise.all解决并行任务

当某个函数需要在 N 个回调都完成时才执行,这个时候就可以使用Promise.all来改善你的代码。

以下是一个图片并行加载的例子,当所有图片加载完成后,再将所有图片一起展示。

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
function loadImg(src){
return new Promise( (resolve,reject)=> {
var img = document.createElement("img");
img.src = src;
img.onload = function(){
resolve(img);
}
img.onerror = function(err){
reject(err);
}
});
}
function showImgs(imgs){
imgs.forEach(function(img){
document.body.appendChild(img);
});
}
Promise.all([
loadImg('1.png'), //加载图片
loadImg('2.png'),
loadImg('3.png'),
...
]).then(showImgs); //显示图片

promise

需要注意的是,Promise.all中传入的 Promise 数组,各自 resolve 之后得到的值,将合并成一个数组传入到 then 中的方法,且数组中 resolve 值的顺序,与 Promise 数组的顺序一致。

3.Promise.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
function getUserInfo(){
console.log('getUserInfo start');
return new Promise((resolve,reject)=>{
setTimeout(()=>{
var userInfo = {
name : 'adamchuan'
};
resolve(userinfo);
console.log('getUserInfo end');
},1000);
});
}
function getGroupInfo(userinfo){
console.log('getGroupInfo start');
return new Promise((resolve,reject)=>{
setTimeout(()=>{
var groupInfo = {
name : 'jdc'
}
console.log('getGroupInfo end');
resolve(groupInfo,userinfo);
},1000);
});
}
function getTaskInfo(groupInfo,userinfo){
console.log('getTaskInfo start');
return new Promise((resolve,reject)=>{
setTimeout(()=>{
var taskInfo = {
name : 'rebuild'
};
console.log('getTaskInfo end');
resolve();
},1000);
});
}
var p = Promise.resolve();
p.then(getUserInfo)
.then(getGroupInfo)
.then(getTaskInfo);
/* 输出结果
getUserInfo start
getUserInfo end
getGroupInfo start
getGroupInfo end
getTaskInfo start
getTaskInfo end
*/

如上面代码所示,我们可以很清楚的理解到程序执行的顺序是

promise

  1. 得到userInfo
  2. 得到groupInfo
  3. 得到taskInfo

但是如果我们对代码进行一点小的改造,将 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
var p = Promise.resolve();
p.then(getUserInfo)
.then(getGroupInfo)
.then(getTaskInfo)
...
function getUserInfo(){
console.log('1');
new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('2');
resolve();
},1000);
});
}
function getGroupInfo(){
console.log('3');
new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('4');
resolve();
},1000);
});
}
function getTaskInfo(){
console.log('5');
new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('6');
resolve();
},1000);
});
}
/* 输出结果
getUserInfo start
getGroupInfo start
getTaskInfo start
getUserInfo end
getGroupInfo end
getTaskInfo end
*/

这是为什么呢?
因为每次调用 then 都会返回一个新的 Promise ,如果 then 中的申明的方法没有返回一个 Promise ,那么会默认返回一个新的
处于 fulfilled 的 Promise ,之后添加的 then 中的方法都会立即执行,所以执行的顺序就变成这样了:
promise

当要在使用链式 Promise 时,请务必在then传入的方法中返回一个新的 Promise。

另外一个需要注意的是,resolve 传递给下个 then 方法的值只能有一个,上面 getTaskInfo 方法中是无法获取到 userInfo 的值,所以如果有多个值需要放在一个数据集合( Array , Object , Map , Set )中传入下个方法。

1
2
3
4
function getTaskInfo(groupInfo,userInfo){ /* userInfo为undefined */
console.log(groupInfo); // { name : 'jdc'}
console.log(userInfo); // undefined
}

4.中断或取消 Promise 链

Promise 标准的API 中并没有提供相应的方法来 中断或者取消 Promise 链的执行,一些库中提供了类似Promise.break或者 Promise.fail的方法来中断或取消 Promise 链。利用Promise.catch的特性来中断 promise链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 用于中断的信号 */
class BreakSignal { }
Promise
.then(() => {
// 开始.
})
.then(() => {
if (wantToBreakHere) {
// 抛出中断信号.
throw new BreakSignal();
}
})
.then(() => {
// 需要跳过的部分.
})
// 接住中断信号.
.catch(BreakSignal, () => { });

只要在 Promise 执行过程中抛出异常,都会直接跳转到 catch 中。但是这样的做法有一个缺点,无法区分程序本身的异常,还是手动抛出的异常。所以需要手动设置一个标识标量,来区分是为了中断执行还是本身的程序异常。

小结

合理的利用 Promise ,可以让代码脉络更加的清晰易懂,流程控制,异常捕获也更加准确。当然为了使用 Promise 也要编写很多额外代码,
想要真正的解决回调问题还得期待ES7的 async await 关键字的到来,不过在此之前,Promise 都将是解决程序流程控制的最优选择之一。

参考文章

  1. Promise A+
  2. Promise迷你书
  3. Promise的前世今生
  4. jQuery的deferred对象详解