Promise理解进阶篇
1、Promise状态变化
首先看下面代码:
1 | function doubleUp(value) { |
这个例子中我们先看这一句:
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 | var onRejected = console.error.bind(console); |
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 | main().then(function (results) { |
传递给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 | function createPromise(time){ |
使用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
23function 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 | const timeout = (ms) => new Promsie((resolve, reject) => { |
下面的关键就是定义 sequenceTasks 函数。
1 | function sequenceTasks(tasks) { |
这样一个顺序执行就基本实现了,如果觉得在实现上不好看,可以用 Array.prototype.reduce 来改进
1 | function sequenceTasks(tasks) { |