我们都知道async/await是基于generator的语法糖,毕竟声明一个generator函数及手动调用next()方法是一件很麻烦的事情,对于程序员来说最幸福的事情,就是能吃到语法糖哈哈哈。不过刚入门这部分内容的时候,我也踩过一点关于async和await的坑,是时候结合事件循环去说说了。

事件循环和async/await

JavaScript在设计之初就被设计为是一门单线程的语言,它依赖事件循环来管理异步操作。asyncawait关键字允许以更类似同步的方式编写异步代码,而不阻塞主线程。对,你没看错,这里准确来说,只是更类似同步的方式,也就是说异步操作仍然在悄悄的发生着,我们只是表面看上去像是一段同步执行的代码。

先来看一下这段代码

我们分析一下这段代码的执行顺序:

1.console.log(‘0. 脚本开始’); 毫无疑问,这是一个同步任务,正常输出0.脚本开始

2.执行exaFunc(),此时发现该函数使用了async关键字声明,进入exaFunc()的执行上下文,创建作用域和作用域链

3.exaFunc()中的console.log(‘1. 开始执行异步函数’),控制台正常输出

4.遇到了await关键字,代码需要等待await后面的代码,此时整个exaFunc()会暂时停止执行后续代码

5.继续执行同步任务,控制台输出2. 异步函数已调用,继续执行脚本中的其他任务’

6.此时Promise状态因为resolve的关系变成了fulfilled,await使命完成,继续执行exaFunc()后面的代码,控制台输出异步操作完成

分析完成,总结一下原理

  • 事件循环:通过执行任务、处理事件和排队回调函数来实现非阻塞I/O操作。
  • async函数:声明一个返回Promise的函数。在其中,await会暂停函数执行,直到等待的Promise解决。
  • await:暂停异步函数的执行并等待Promise解决。然后继续执行,返回解决的值。

await与事件循环的交互

  1. 遇到awaitasync函数的执行暂停。
  2. 释放线程:控制权返回给事件循环,允许运行其他任务。
  3. 等待:事件循环等待Promise解决。
  4. 恢复执行:一旦Promise解决,事件循环将异步函数剩余的部分放回任务队列中,从await暂停的地方恢复执行。

错误处理

  • try...catch:在async函数内部使用try...catch结构来捕获异常,是处理错误的直接方式。

这条很好理解,因为不捕获异常的后果就是,后续代码不会再继续执行就被中断了,当然也可以在async函数返回的Promise调用链的末尾使用.catch()方法来处理错误。

并行执行

  • Promise.all():当有多个互不依赖的异步操作时,使用Promise.all()并行执行这些操作,以节省时间。

这里一定要注意,还是以上面的代码为例,我再次使用了await等待两个异步操作,但发现实际上这几个操作是互不依赖的,这样的等待完全是徒劳的,我们可以使用Promise.all()来处理这样的情况。

避免无谓的等待

  • 不要滥用await,对于不依赖前一个异步操作结果的代码,不必使用await等待。

await在循环中的使用

  • 尽量避免在循环中使用await等待异步操作,这会导致异步操作串行执行,降低程序效率。如果可能,考虑并行处理。

函数签名

  • 使用async关键字的函数会返回一个Promise。即使函数内部直接返回非Promise值,该值也会被包装成一个已解决(resolved)的Promise。比如await 0,实际上就是promise.resolve(0)

结论

语法糖虽好,但还是不能乱吃,要按照基本法吃。