深入讲解js异步编程的async与await关键字

141 阅读5分钟

了解异步编程是掌握JS这门语法的关键,而通过async与await关键字可以更加直观理解,async与await是基于Promis的语法糖,使用async与await可使代码更易读,更易维护

想详细了解Promise与异步概念可参考js入门指南之Promise:从''承诺''到理解,告别回调地域

一、async:标记异步

1. 在声明的一个函数前加上async,该函数则会成为异步函数,也可称为async函数

function fn(){
   console.log('1')    //函数fn为普通函数(即同步函数)
  }
  
  
async function foo(){   //函数foo为异步函数   
   console.log('2')
  }



2. 被async标记的异步函数总是会返回一个Promise对象

被async标记的异步函数在执行到最后时,会默认执行Promise.resolve(),此时,该函数内若有一个return的值,则该值会被Promise.resolve(),包装为一个‘成功状态’的Promise对象,该对象若调用。then()方法,则会将‘成功状态’与该值(如果有值的话),继承给.then()内的函数体。若async函数中被抛出了一个错误,则该函数在执行完毕后会返回一个‘失败状态’的Promise对象

async function fn(){
    
}
fn().then(() => {
    console.log('被async标记的函数默认返回成功状态的Promis')
})
async function fn(){
    return 1
}
fn().then((num) => {
    console.log(num)  //打印结果为1,  被async标记的函数在返回时的实例对象,
                     // 还会将该函数return的值一起传给.then()的函数体
})

二、await关键字:依赖于async关键字,用于等待结果

1. await关键字只能用在async函数内部使用,该关键字的作用是,当async内部函数代码在执行时,遇到了await关键字,若await关键字后是一个Promise对象,则会发生以下步骤

(1)async函数内部的执行流程将在此暂时中断,await后的代码将暂时停止执行

(2)async函数被暂停后,引擎开始执行Promise内的代码,当接收到(fulfiled或者rejected),则会获得获得返回的Promise对象,同时将开始执行该async函数外部的代码,并且准备恢复执行async函数内的代码。

(3)值得注意的是,当v8引擎只有等待完await后的Promise返回的最终状态,才会准备恢复执行async函数内的代码,这一行为会被视为一个微任务,放置在该线程任务的异步任务队列中的微任务队列中等待执行。

简单来理解上面四条步骤就是,在遇到await关键字,将中断async函数内代码的执行,若其后跟着一个Promise对象时,v8引擎直接开始执行该Promise内的代码,该执行顺序逻辑遵循外部代码的执行顺序逻辑,(即遇到同步代码直接执行,遇到异步代码则暂时挂起),当Promise内的同步代码执行完毕,或读取到了resolve()时,才会开始准备恢复执行async函数所在的执行上下文代码,但在此之前,引擎会先执行async函数外的代码

这样的执行顺序可以避免造成程序卡死,或暂时终止执行async函数内的部分代码

这样的执行顺序的原因是因为js是一个单线程语言,如果没有异步的编程,使用同步的方式去等待一个耗时操作,整个页面将失去响应,await这一机制,使得js线程在等待异步操作的结果时,可以先去执行异步函数外的操作,这依赖于js中的event-loop(事件循环)机制。

由event-loop与本章所提知识可知,下列代码打印结果为:script start,promise,script end,then1,then2,setTimeout,1,2,async1 end

console.log('script start');
async function async1() {
  await async2()
  console.log('async1 end');
}
async function async2() {
 await ((function () {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log('1');
        resolve()
      }, 2000)
    })
  })())
   console.log('2')
}
async1()
setTimeout(() => {
  console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
  .then(() => {
    console.log('then1');
  })
  .then(() => {
    console.log('then2');
  });
console.log('script end');

若想详细了解event-loop机制,宏任务,微任务,异步等,可观看一篇文章让你理解透彻js事件循环(eventloop)

2. 当async内部函数代码在执行时,若await关键字后是一个非Promise对象(如数字,字符串等)

其await的行为与后面是一个Promise对象会显著不同,此时会发生以下步骤

(1)js此时会自动使用promise.resolve(),并且会将await 后的值直接包装成一个立即解决(resolve)的Promise,并且开始执行async函数外的代码。

(2)虽然await后面的值并没不耗时,即不需要等待,但是这依然会导致异步,既使得await之后的代码依然会被丢入异步任务中的微任务队列中等待执行

如以下代码,执行结果顺序为:script start,promise,script end,1,then1,async1 end,then2,setTimeout

const num = '1'

console.log('script start');
async function async1() {
  await async2()
  console.log('async1 end');
}
async function async2() {
 await num
 console.log(num)
}
async1()
setTimeout(() => {
  console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
  .then(() => {
    console.log('then1');
  })
  .then(() => {
    console.log('then2');
  });
console.log('script end');

3. 若await后面的Promise最终为rejected(拒绝状态),则await会返回一个异常,这个一次需要使用trya...catch块来捕获和处理,否则将导致程序错误

三、总结

总而言之,await会暂停async函数,直接进入async后的值的代码,并在执行完所有的代码后才会开始准备恢复执行async函数所在的执行上下文代码(无论await后的代码是否耗时,耗时多久,都会等待全部执行完毕或读取到最终状态时才会开始准备恢复执行async函数所在的执行上下文代码),此时await将会把它后面的async的函数代码置入微任务队列中进行等待,并且v8引擎开始执行该async函数外的代码

async与await基于事件循环及微任务,宏任务,异步的等待机制,通过微任务调度恢复的执行方式,保证了程序能够及时响应,而不至于导致程序卡死