ECMA的新特性很多,可以参考,这里介绍其中的一个用于解决异步回调的特性–异步函数。
前置基础知识
在了解异步函数之前,你需要了解一些必备的基础知识,比如,什么是异步操作,什么是回调函数,什么是Promise,以及Promise与回调函数的优缺点。
异步回调可以参看这篇文章:《理解单线程JS中的异步回调》
Promise与回调的异步操作,可以参看这篇文章:《Javascript异步编程的4种方法》
在了解了基础知识后我们就来看看怎么定义异步函数。
异步函数的定义
非常简单,使用async关键字修饰函数即可。如下:
async function a() { console.log(1); return 3; }
如果加上es6的箭头函数,它的定义还可能有下面几种:
let a = async ()=>{};
let b = async function(){};
class C(){ async c(){} }
异步函数的返回值
异步函数的返回值是一个Promise对象。如果函数执行成功,这个Promise对象的resolve回调会填充为函数return的返回值;如果函数内抛错,这个错误会被异步函数捕捉到,并填充为Promise对象reject回调的参数。
具体请看如下的两个示例。
没有异常的示例:
async function a() { return 3; } let execA = a(); console.log(execA); execA.then((res)=>{console.log('resolve',res)},(err)=>{console.log('reject',err)});
执行上面这个代码,控制台会输出如下结果
Promise { 3 } resolve 3
示例可以看出来execA是函数a的返回值,它是一个Promise对象。并且这个Promise的resolve回调参数被函数a的返回值3填充。
有异常的示例:
async function a() { throw new Error('This is an error!'); return 3; } let execA = a(); console.log(execA); execA.then((res)=>{console.log('resolve--> ',res)},(err)=>{console.log('reject-->',err)});
这个例子仅仅是在异步函数内抛了异常
输出结果如下:
Promise { <rejected> Error: This is an error! at a (F:\good-good-study\async-await\async-await.js:64:8) at Object.<anonymous> (F:\good-good-study\async-await\async-await.js:67:13) at Module._compile (module.js:641:30) at Object.Module._extensions..js (module.js:652:10) at Module.load (module.js:560:32) at tryModuleLoad (module.js:503:12) at Function.Module._load (module.js:495:3) at Function.Module.runMain (module.js:682:10) at startup (bootstrap_node.js:191:16) at bootstrap_node.js:613:3 } reject--> Error: This is an error! at a (F:\good-good-study\async-await\async-await.js:64:8) at Object.<anonymous> (F:\good-good-study\async-await\async-await.js:67:13) at Module._compile (module.js:641:30) at Object.Module._extensions..js (module.js:652:10) at Module.load (module.js:560:32) at tryModuleLoad (module.js:503:12) at Function.Module._load (module.js:495:3) at Function.Module.runMain (module.js:682:10) at startup (bootstrap_node.js:191:16) at bootstrap_node.js:613:3
可以看到,函数a执行之后,返回值依然是一个Promise,不过这个Promise的resolve回调没有被调用,而reject回调被执行了。
await修饰符介绍
await关键字只能在异步函数中使用。
如果await修饰的是一个变量或者常量,返回值不变;
如果await修饰的是一个Promise,它的作用是阻塞await之后的代码,等待被修饰的Promise执行完成之后,返回resolve或者reject的结果,await之后被阻塞的代码才会执行。
关键点有三个:一是await只能在函数内使用,二是该函数只能是异步函数,即有async修饰的函数,三是能阻塞之后的代码,将Promise转换为同步执行。
比如,一个没有await修饰的Promise示例如下:
async function a() { // 新建一个异步操作 let p = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },1000); }); // 执行异步操作 p.then((res)=>{ console.log('p resolve--> ',res,Date.now()); },(err)=>{ console.log('p reject-->',err); }); // 函数结束之前的打印 console.log('fn a() end!'); return 3; } let execA = a(); console.log('a() return val-->',execA); execA.then((res)=>{console.log('a resolve--> ',res,Date.now())},(err)=>{console.log('a reject-->',err)});
这段代码的执行结果如下:
fn a() end! a() return val--> Promise { 3 } a resolve--> 3 1524908937320 p resolve--> 1 1524908938319
可以看到,函数a执行完之后,我们的异步操作才开始执行,并且操作p延时了1s再触发。
如果这里有个需求,打印“fn a() end!“需要发生在p操作完成之后,那么不用await关键字时,a函数可能会改造成如下形式:
async function a() { // 新建一个异步操作 let p = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },1000); }); // 执行异步操作 p.then((res)=>{ console.log('p resolve--> ',res,Date.now()); // 异步操作完成后,再打印结束标识 console.log('fn a() end!'); },(err)=>{ console.log('p reject-->',err); }); return 3; }
这种方式就是将输出日志在回调中调用,这是非常不好的写法。
如果p操作之前还有很多其他操作,比如我还有个异步q操作,那么回调的层次就会非常深。比如:
async function a() { // 新建异步操作p let p = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve('p'); },1000); }); // 新建异步操作q let q = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve('q'); },2000); }); // 执行p操作 p.then((res)=>{ console.log('p resolve--> ',res,Date.now()); // 执行q操作 q.then((res)=>{ console.log('q resolve--> ',res,Date.now()); // p和q都执行完之后,再打印结束标志 // 这里的回调存在嵌套,不好不好! console.log('fn a() end!'); },(err)=>{ console.log('q reject-->',err); }); },(err)=>{ console.log('p reject-->',err); }); return 3; }
上面这段代码有个很不好的地方,那就是存在回调嵌套,虽然promise可以合并(all方法),但是使用起来还是很不方便。下面就来看看await怎么改造这些回调。
使用await配合async改造回调
直接上代码,如下:
async function a() { // 新建异步操作p let p = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve('p'); },1000); }); // 新建异步操作q let q = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve('q'); },2000); }); // 等待p执行 let resultP = await p; // 等待q执行 let resultQ = await q; console.log('p resultP --> ',resultP ,Date.now()); console.log('q resultQ --> ',resultQ ,Date.now()); // 打印结束标志 console.log('fn a() end!'); return 3; }
可以看到,上面代码非常的简洁,就跟我们写同步代码一样,p和q都是一个promise,使用await,等到p、q处理完异步操作之后,下面的打印日志才会执行。所以,控制台输出如下
p resultP --> p 1528182477525 q resultQ --> q 1528182477527 fn a() end!
总结
异步函数的强大之处就是将我们串行的Promise操作改造为同步执行,这样能大大提高代码的可维护性。Promise和异步函数都已经被nodejs内置了,笔者这里用的版本为v9.2.0,以上代码的测试都是在该版本下进行的。相信在不久的将来,我们的浏览器端,比如chrome,firefox等,都会新增该es标准。
打赏作者