1、Promise状态变化

首先看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出现异常的时候会被调用
console.error(error);
});

这个例子中我们先看这一句:

1
var promise = Promise.resolve(1);

意思是决议promise的状态,在promise决议之后,会立刻异步调用两个处理函数中的一个,比如是fulfilled状态,则将then的onFulfilled回调函数放入microTask队列,如果是reject状态,则将调用onRejected回调函数放入microTask队列。
increment函数有一个参数,这个参数是由Promise.resolve(1)传入的。进入increment函数后,可以看到return value+1,然后increment函数到此为止,其实没这么简单,return的值会由Promise.resolve(return的返回值); 进行相应的包装处理,不管回调函数返回或者不返回,then都会返回一个新建的promise对象,当回调函数有return返回值时生成一个promise,[[PromiseStatus]]是”resolved”,[[PromiseValue]]是return的返回值。当回调函数没有return时,生成的promise,[[PromiseStatus]]是”resolved”,[[PromiseValue]]是undefined。
resolve的中文是决议,决议的结果可能是完成也可能是拒绝。比如可以向resolve中传递一个Promise.reject的返回值,那么决议的结果就是拒绝。reject不像resolve会将传入的值展开,如果向reject传入一个promise/thenable值,它就会原封不动的将这个值设置为拒绝理由。

2、catch和then(onRejected)的区别

try…catch很好但是无法用于异步的错误捕获。node中的error-first风格可以用于异步的错误处理,但是无法很好的组合,需要写很多if判断。promise没有采用error-first风格,而是用来分离回调风格,一个回调用于完成情况,一个回调用于拒绝情况。
那么catch和then(onRejected)这两种捕获异常的方法,咱们用哪一种呢?
首先要考虑的是兼容问题。在IE8以下,使用的是ES3规范,在ECMAScript 3中保留字是不能作为对象的属性名使用的,因此不能将catch作为属性来使用。而现在的浏览器都是基于ECMAScript 5的,而在ECMAScript 5中保留字都属于IdentifierName,也可以作为属性名使用了。在es3中实在想用,可以使用中括号标记法(bracket notation)的话,则可以将非合法标识符作为对象的属性名使用。或者我们不单纯的使用catch,而是使用then也是可以避免这个问题的。
还有一个区别是,then中的onRejected是不能捕获到onFulfilled中抛出的错误,而catch可以。

3、使用reject而不是throw

在promise中并不需要通过throw来抛出一个错误,完全可以用reject来代替。
Promise的构造函数,以及被then调用执行的函数基本上都可以认为是在 try…catch 代码块中执行的,所以在这些代码中即使使用throw,程序本身也不会因为异常而终止。
如果在Promise中使用throw语句的话,会被 try…catch 住,最终promise对象也变为Rejected状态。
如果使用throw我们很难区分到底是程序抛出异常,还是人为抛出,在浏览器的调试功能中有一个在程序发生异常的时,自动break调试的功能,如果我们人为抛出一个错,浏览器开启这个功能时也会自动break调试,从而影响浏览器提供此功能的正常使用。
如果想在then中进行reject,怎么办呢?
当然是定义一个promise,并返回,比如:

1
2
3
4
5
var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error("this promise is rejected"));
}).catch(onRejected);

4、异步错误处理

既然上面两节提到了错误处理,这一节我们来研究关于异步的错误处理。
使用try…catch只能进行同步的错误处理

5、Promise.all和Promise.race

Promise.all接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用.then方法。
比如:

1
Promise.all([request.comment(), request.people()]);

request.comment()和request.people()会同时执行,这两个promise的结果会按照顺序返回,即在下一个then的onFulfilled回调函数中接受的参数首先是一个数组,然后这个数组的顺序和Promise.all参数数组的顺序一致,所以

1
2
3
main().then(function (results) {
console.log(results); // 按照[comment, people]的顺序
});

传递给Promise.all的promise并不是一个个的顺序执行的,而是同时开始、并行执行的。
Promise.all在接收到的所有的对象promise都变为FulFilled或者Rejected状态之后才会继续进行后面的处理,与之相对的是Promise.race只要有一个promise对象进入FulFilled或者Rejected状态的话,就会继续进行后面的处理。Promise.race在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。
如何模拟Promise.all,就是使用一个count计数,当count===arr.length时,resolve一个数组:

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 createPromise(time){
return () => new Promise(function(resolve, reject){
setTimeout(function(){
console.log(`delay: ${time}`);
resolve(time);
}, time);
});
}
function creatPromiseAll(arr){
let count=0;
let res = [];
return new Promise(function(resolve, reject){
for(let i=0;i<arr.length;i++){
arr[i]().then(function(value){
count++;
res[i] = value;
if(count===arr.length){
console.log(`delay all`);
resolve(res);
}
});
}
})
}
creatPromiseAll([createPromise(500), createPromise(1000), createPromise(1500)]);

使用await模拟实现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
function createPromise(time){
return () => new Promise(function(resolve, reject){
setTimeout(function(){
console.log(`delay: ${time}`);
resolve(time);
}, time);
});
}
function creatPromiseAll(arr){
let count=0;
let res = [];
return new Promise(function(resolve, reject){
let newArr = arr.map(async (item, i, arr) => {
let value = await item();
value.then(val=>{
return val;
});
});
console.log(newArr);
resolve(newArr);
});
}
creatPromiseAll([createPromise(500), createPromise(1000), createPromise(1500)]);

6、Promise顺序执行

Promise.all方法,传入元素为Promise的数组,all方法开始执行时,所有的Promise同时执行,不分先后,当数组中的每一个Promise状态都变成resolve或reject时,这个方法才算执行完。
Promise.all可以提供同时执行,但是无法提供顺序执行。
下面就以请求url为例,来实现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
const timeout = (ms) => new Promsie((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});

const ajax1 = () => timeous(2000).then(() => {
console.log('1');
return 1;
});
const ajax2 = () => timeous(1000).then(() => {
console.log('2');
return 2;
});
const ajax3 = () => timeous(2000).then(() => {
console.log('3');
return 3;
});
function main() {
// tasks中的不是Promise的数组,而是方法
var tasks = [ajax1, ajax, ajax3];
sequenceTasks(tasks).then(function (results) {
console.log(results);
}).catch(function(err) {
console.error(err);
})
}

下面的关键就是定义 sequenceTasks 函数。

1
2
3
4
5
6
7
8
9
10
11
12
function sequenceTasks(tasks) {
var results = [];
var resultSave = function (value) {
results.push(value);
return results;
}
var promise = Promise.resolve();
for(let i in tasks) {
let task = tasks[i];
promise = promise.then(task).then(resultSave);
}
}

这样一个顺序执行就基本实现了,如果觉得在实现上不好看,可以用 Array.prototype.reduce 来改进

1
2
3
4
5
6
7
8
9
10
function sequenceTasks(tasks) {
var results = [];
var resultSave = function (value) {
results.push(value);
return results;
}
tasks.reduce(function(promise, task) {
return promise.then(task).then(resultSave);
}, Promise.resolve());
}

7. promise相比于普通回调的性能