基本概念
async包起来的代码块(或者函数)代表执行这块代码的时候,是异步执行的,也就是会先执行这个代码块下面的代码,然后再慢慢执行这个代码块里面的代码。(有点类似java中开启新Thread执行Runnable,async中的代码就是Runnable中的代码)
await用于执行async函数,并且await也只能在async代码块中使用。用await执行async函数,代表在整个async代码块中,会等待await后面的async函数异步执行得到结果后,才继续往下执行await下面的代码。也正是因为这个等待的特性,所以await也只能在async的异步代码块中使用。
在Parse中创建如下两个云函数,调用后会发现先得到了输出,也就是执行了return,然后2s后控制台打印出了log。
Parse.Cloud.define('nnn', async function(req) { (async ()=>{ setTimeout(() => { console.log('log'); }, 2000); })(); return 'return'; });
Parse.Cloud.define('mmm', async function(req) { (async ()=>{ console.log(await mmm2()); })(); return 'return'; }); function mmm2() { return new Promise(((resolve, reject) => { setTimeout(() => { resolve('log'); }, 2000); })) }
async函数本身return promise
1. 没有显式return,相当于return Promise.resolve(undefined);
2. return非Promise的数据data,相当于return Promise.resolve(data);
3. return Promise, 会得到Promise对象本身
async函数内部若需要做异步的事情,比如定时器结束之后异步返回结果,需要在这个async内部通过return new Promise((resolve, reject)=>{})返回Promise,并在得到结果后resolve,参考0002()。但是如果异步的事情也是一个async函数,也就是说async函数内通过await调用其他async函数,可以直接return,参考000()。
// 延时2s返回log Parse.Cloud.define('ooo', async function(req) { return await ooo(); }); async function ooo() { return await ooo2(); } async function ooo2() { return new Promise(((resolve, reject) => { setTimeout(() => { resolve('resolve'); // reject('reject'); }, 2000); })) }
如果调用async函数时不加await,则只相当于在调用的地方插入了async代码块异步执行,并不会等待结果再继续执行。同时因为没有通过await调用意味着该async函数中的异常无法被外部捕获,如果抛出异常将会引发UnhandledPromiseRejectionWarning报错。
// async的函数不通过await调用,会直接输出return,2s后打印log Parse.Cloud.define('rrr', async function(req) { rrr(); return 'return'; }); async function rrr() { return new Promise(((resolve, reject) => { setTimeout(() => { console.log('log'); resolve(); }, 2000); })) }
利用这个特性,可以在需要异步调用某个函数并且并不需要这个函数返回值的时候,将这个函数声明为async,并在调用的地方不加await。需要注意的地方是该函数内部必须捕获异常,否则外部调用时没有await是无法捕获异常的。
异常处理
throw必须被层层捕获,如果某一层没有捕获,就会出现UnhandledPromiseRejectionWarning报错。
在Promise中可以直接使用throw error来抛出异常,效果跟reject(error)一致。在async函数内也可以直接使用throw来抛出异常。reject和throw都会被上一层的调用者捕获。
如果只使用非代码块的async函数和await,不需要层层手动捕获异常,会自动上抛。如下代码无需手动try-catch,最终抛出throw。
Parse.Cloud.define('ppp', async function(req) { return await ppp(); }); async function ppp() { return await ppp2(); } async function ppp2() { throw 'throw'; }
如果是async代码块,throw的异常并不会被捕获,只能在async代码块内部try-catch自行捕获。如果要将异常再次抛给外部,需要在外部包一层Promise,并且使用Promise的reject来抛出异常。
async代码块内部的异常无法通过外部的try-catch捕获,就像java中创建新Thread执行Runnable时无法通过Thread外部的try-catch捕获Runnable中的异常一样。
没有处理async中的异常:
new Promise((resolve, reject) =>{ (async()=>{})() })
捕获异常成功:
new Promise((resolve, reject) =>{ (async()=>{ try{ ... resolve() } catch(e){ reject(e) } })() })
捕获失败:
new Promise((resolve, reject) =>{ try{ (async()=>{ // 这里的异常无法在async外部被捕获,所以这样做会报错 throw '' })() } catch(e) { reject(e) } })
Promise中通过await调用的其他async函数,也必须去捕获它的异常,并且放进reject,否则会出现UnhandledPromiseRejectionWarning报错。
便利性
从使用回调式改为使用async/await形式的Promise之后,最明显的改观是代码量变少了,不再有回调地狱。当然相应的,在还没有完全习惯之前,写代码或者看代码出错的概率会相应提高。
代码调试上,不再需要像之前那样在回调中设置断点,只需要单步到底,因为IDE的单步调试能完美适应async/await。
递归上,之前的回调式中,要实现递归,需要写两个函数,通过Callback调用实现递归,代码可读性比较低,每次要写递归的时候还需要想一下。现在使用了async/await之后,甚至在原来需要用递归的地方,只需要使用循环就能搞定。