es2017新特性异步函数async、await简单介绍

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标准。



打赏作者

发表评论

电子邮件地址不会被公开。 必填项已用*标注