Promise与Async await
一、使用Promise的原因
解决回调地狱问题(代码臃肿、可读性差、耦合度过高、复用性差)
二、什么是Promise
Promise是异步编程的一种解决方案,promise异步回调,可以避免层层嵌套回调。
Promise对象是一个构造函数,用来生成Promise实例。Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。
Promise有三种状态:pending(执行状态)、fulfilled(成功时的状态)、rejected(失败时的状态)。当promise状态发生改变,就会触发then()里面相应的回调函数处理后续步骤,当promise状态已经改变,则不会更改。
var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('执行完成'); resolve('随便什么数据'); }, 2000); });
运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:
function runAsync(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('执行完成'); resolve('随便什么数据'); }, 2000); }); return p; } runAsync().then(function(data){ console.log(data); //后面可以用传过来的数据做些其他操作 //...... });
我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。
三、Promise的几种方法
(1)Promise.then():用于注册状态变为fulfilled或者reject时的回调函数,接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。then()方法返回的是一个新的Promise实例,因此可以采用链式写法。
const promise = new Promise((resolve, reject) => { resolve('fulfilled'); // 状态由 pending => fulfilled }); promise.then(result => { // onFulfilled console.log(result); // 'fulfilled' }, reason => { // onRejected 不会被调用 })
(2)Promise.catch():在链式写法中可以捕获then中发送的异常。
一般来说,不要在then()里面定义rejected状态的回调函数(即then的第二个参数),而应该使用catch()方法。理由是catch()方法可以捕获前面then()方法执行中的错误,也更接近同步的写法(try/catch)。
和传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise对象抛出的错误不会被传递到代码外层,即不会有任何反应。
在someAsyncThing函数中产生Promise对象,内部有语法错误,浏览器运行到这一步会打印错误,但是并不会退出进程、终止脚本执行,2s后还是输出123。也就是说Promise内部的错误并不会影响到Promise外部的代码,即“Promise会吃掉错误”。
(3)Promise.all():可以将多个Promise实例包装成一个新的Promise实例。多个Pomise任务同时执行,如果全部执行成功,则以数组的方式返回所有Promise任务的执行结果。如果有一个Promise任务rejected,则只返回第一个rejected任务的结果。
Promise.all([p1,p2,p3])接受一个数组作为参数,数组中的p1,p2,p3都是promise实例/。
let p1=new Promise((resolve,reject)=>{ resolve('p1成功'); }) let p2=new Promise((resolve,reject)=>{ resolve('p2成功'); }) let p3=Promise.reject('p3失败'); Promise.all([p1,p2]).then((res)=>{ console.log(res);// ['p1成功','p2成功'] }).catch((err)=>{ console.log(err); }) Promise.all([p1,p2,p3]).then((res)=>{ console.log(res);//p3失败 }).catch((err)=>{ console.log(err); })
注意:
Promise.all()获得的成功结果的数组里面的数据顺序与Promise.all()接收参数的顺序是一致的,即P1的结果在前,即使P1的结果获取的比P2晚。
这带了一个很大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个异步请求要求按照发送的顺序获取数据,使用Promise.all()可以解决这个问题。
(4)Promise.race():race赛跑的意思,多个Prromise任务同时执行,返回最先执行结束的Promise任务的结果,不管这个Promise结果是成功还是失败。
注意:Promise.race在第一个Promise对象变为fulfilled之后,并不会取消其他promise对象的执行。只是只有先完成的Promise才会被Promice.race后面的then()处理,其他promise还是在执行的,只不过不会进入到promise.race后面的then里。
let p1=new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('p1成功'); },2000) }) let p2=new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('p2成功'); }) },500) Promise.race([p1,p2]).then((res)=>{ console.log(res);//p2成功 }).catch((err)=>{ console.log(err); })
四、Async/await
async关键字
async function asyncFn() { return 'hello world'; } asyncFn();
返回结果:
关于async/await的使用规则:
(1)async函数的返回类型为Promise对象。
当返回的是promise对象时,正好符合async函数的返回类型;
当返回的是值类型时,相当于Promise.resolve(data);还是一个Promise对象,但是在调用async函数的地方通过简单的=是拿不到这个data返回值的,因为返回值是一个Promise对象,需要用.then(data={console.log(data)})函数才能拿到。
如果没有返回值,相当于返回了Promise.resolve(undefined);
(2)非阻塞
async函数里面如果有异步过程会等待,但是async函数本身会马上返回,不会阻塞当前线程,可以理解为,async函数工作在主线程,同步执行,不会阻塞页面渲染。async内部由await关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回。
(3)无等待
async表明程序中可能有异步过程,async的异步体现在await上。在没有await的情况下执行async函数,他会立即执行,返回一个Promise对象,并且不会阻塞后面的语句。
(4)await必须放在async函数内部使用,不能单独使用;
(5)await关键字后面跟Promise对象,async函数必须等到内部的所有await命令的Promise对象执行完,才会发生状态改变。
await后面的promise对象不必写then,因为await的作用之一就是获取后面promise对象成功状态传递出来的参数。
执行过程:
await是让出线程的标志,当执行到await时,await后面的函数会先执行一遍,然后再跳出整个async函数来执行后面的JS栈代码。等本轮时间循环执行完了之后又跳回到async函数中等待await后面表达式的返回值,如果返回值为非Promise则继续执行async后面的代码,否则将返回的Promise放入Promise队列。
async/await的错误处理方法
await可以直接获取后面promise对象成功状态传递的参数,但是却捕捉不到失败的状态。在这里,通过给async函数添加then()/catch()方法来解决,因为async函数本身就会返回一个promise对象。
一个包含错误处理的完整例子:
function fun2(){ return new Promise((resolve,reject)=>{ var num=Math.random(); if(num<=0.5){ resolve('success'); } else{ reject('failed'); } }) } async function fun1(){ console.log('async函数'); let res=await fun2(); //注意:在async函数里,必须要将await的结果return出去,否则的话执行then()返回值都为undefined return res; } fun1().then(data=>{ console.log(data); }).catch(error=>{ console.log(error) })
五、Async/await相比于Promise的优势
(1)能更好地处理then()链式调用,代码更加优雅,几乎和同步代码一样。
(2)简洁。使用Async/await明显节省不少代码,我们不需要写.then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的data变量。
(3)中间值
假如调用promise1,然后使用promise1的返回值去调用promise2,然后使用两者的结果去调用promise3。
使用promise:
function promise1(){ return new Promise((resolve,reject)=>{ resolve('p1'); }) } function promise2(val){ return new Promise((resolve,reject)=>{ resolve(val); }) } function promise3(val1,val2){ return new Promise((resolve,reject)=>{ resolve(val1+","+val2); }) } function fun(){ return promise1().then((v1)=>{ return promise2(v1).then((v2)=>{ return promise3(v1,v2); }) }) } fun().then((data)=>{ console.log(data); })
使用async/await
async function fun2(){ let v1=await promise1(); let v2=await promise2(v1); return promise3(v1,v2); } fun2().then((data)=>{ console.log(data); })
Promise的局限性
(1)错误会被吃掉
throw new Error('error'); console.log(‘last’); //由于throw error的缘故,代码被终止,所以不会输出'last'。 对于promise let promise = new Promise(() => { throw new Error('error') }); console.log(‘last’);//会正常输出‘last’
说明Promise内部的错误不会影响到Promise外部的代码,这种情况通常称为“吃掉错误”。
这并不是Promise独有的局限性,try...catch也是这样,同样会捕捉一个异常并简单的吃掉错误。
正是因为错误被吃掉,所以Promise中的错误很容易被忽略掉,这也是为什么一般推荐在Promise链的最后添加一个catch方法。
(2)无法取消
Promise一旦创建就会立即执行,无法中途取消。
(3)无法得知pending状态。
当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
六、实现Promise内部原理
以上基本可以实现简单的同步代码,即改变状态操作使用的是同步,但是当resolve在setTimeout内执行时,then时的state还是pending等待状态,这就需要我们在then调用的时候,将成功和失败存到各自的数组,一旦reject或者resolve就调用他们。
完整代码(只能实现一个then,还不能链式调用)
class Promise1{ constructor(fn){ //定义三个状态 this.state='pending';//初始状态 this.value=undefined;//成功的值 this.reason=undefined;//失败的原因 //成功存放的数组 this.onResolvedArr=[]; //失败存放的数组 this.onRejectArr=[]; let resolve=(value)=>{ //用箭头函数改变this指向 if(this.state=='pending'){ this.state='fulfilled'; this.value=value; //一旦执行resove,调用成功数组的函数 this.onResolvedArr.forEach(fun=>fun()) } }; let reject=(reason)=>{ if(this.state=='pending'){ this.state='rejected'; this.reason=reason; this.onRejectArr.forEach(fun=>fun()) } } //自动执行函数 try{ fn(resolve,reject) }catch(e){ reject(e); } } then(onFulfilled,onRejected){ //状态为成功时执行onFulfilled方法并传值 if(this.state=='fulfilled'){ onFulfilled(this.value); } //状态为失败时执行onRejected方法 if(this.state=='rejected'){ onRejected(this.reason); } //当state的状态为pending时将要执行的方法放到数组中和,再调用resolve或reject时执行。 if(this.state=='pending'){ this.onResolvedArr.push(()=>{ onFulfilled(this.value); }) this.onRejectArr.push(()=>{ onRejected(this.reason); }) } } } var p=new Promise1(function(resolve,reject){ setTimeout(function(){ resolve('成功'); },1000) }) p.then(function(data){ console.log(data);//成功 },function(err){ console.log(err) })
Promise.then()的链式调用是通过返回一个新的Promise实现的。
既然jQuery通过返回this实现链式调用,为什么promise.then()不返回this实现呢?
如:
var promise2 = promise1.then(function (value) { return Promise.reject(3) })
假如then函数执行返回this调用对象本身,那么promise2==promise1,promise2的状态也应该等于promise1的状态同为resolved。而onResolved回调函数中返回的是rejected对象,因为Promise中的状态一旦改变就不能更改,所以promise2没办法转成回调函数返回的rejected状态,产生矛盾。
每个promise.then()都返回一个新的promise,返回的新的promise会在上一个状态发生改变后执行,依此类推,每次返回的新的promise被挂在上一个promise上,形成一个串。
七、实现Promise.all()的内部原理
function promiseAll(promises){ if(!Array.isArray(promises)){ throw new TypeError('argument must be array'); } return new Promise((resolve,reject)=>{ let len=promises.length; let index=0; var res=[]; for(let i=0;i<len;i++){//注意定义为let,保留当前的i promises[i].then((data)=>{//执行参数中的promise实例 index++; res[i]=data;//把每一个promise的执行成功结果放入数组中 不能用push()方法,保证返回的顺序 if(index==len){//说明所有promise都执行成功 return resolve(res); } },function(err){ return reject(err); }) } }) } var a1=Promise.resolve(1); var a2=Promise.resolve(2); var a3=new Promise((resolve,reject)=>{ resolve('a3'); }) promiseAll([a1,a2,a3]).then(function(res){ console.log(res);//[1,2,'a3'] }).catch(function(err){ console.log(err); })
八、实现Promise.race()的内部原理
function Race(promise){ return new Promise((resolve,reject)=>{ for(let i=0;i<promise.length;i++){ promise[i].then(resolve,reject); } }) } var p1=new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('2'); },2000) }) var p2=new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('3'); },1000) }) Race([p1,p2]).then(function(val){ console.log(val);//3 })
Promise应用:红绿灯问题
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
Promise链如何终止?
我们想要在resolve的情况下中断或者终止链式调用,主要方法是基于Promise的特点:原Promise对象的状态和新对象保持一致。
我们仅需要在链式调用中,返回一个pending状态或者rejected状态的Promise对象即可。
return (new Promise((resolve, reject)=>{}));//返回pending状态
return (new Promise((resolve, reject)=>{rejcet()}));//返回reject状态 会被最后catch捕获。
.then((number)=>{ console.info(’在这里中断‘); return new Promise((resolve,reject)=>{})//pending状态 //或者 return new Promise((resolve,reject)=>{reject()})//reject()状态 }).then((number)=>{ console.info(`fun4 result:${number}`); })