我们都知道async/await是基于generator的语法糖,毕竟声明一个generator函数及手动调用next()方法是一件很麻烦的事情,对于程序员来说最幸福的事情,就是能吃到语法糖哈哈哈。不过刚入门这部分内容的时候,我也踩过一点关于async和await的坑,是时候结合事件循环去说说了。
事件循环和async
/await
JavaScript在设计之初就被设计为是一门单线程的语言,它依赖事件循环来管理异步操作。async
和await
关键字允许以更类似同步的方式编写异步代码,而不阻塞主线程。对,你没看错,这里准确来说,只是更类似同步的方式,也就是说异步操作仍然在悄悄的发生着,我们只是表面看上去像是一段同步执行的代码。
先来看一下这段代码
async function exaFunc() {
console.log('1. 开始执行异步函数');
await new Promise(resolve => setTimeout(resolve, 0)); // 这里模拟一个异步操作
console.log('3. 异步操作完成');
}
console.log('0. 脚本开始');
exaFunc();
console.log('2. 异步函数已调用,继续执行脚本中的其他任务');
// 输出顺序:
// 0. 脚本开始
// 1. 开始执行异步函数
// 2. 异步函数已调用,继续执行脚本中的其他任务
// 3. 异步操作完成
我们分析一下这段代码的执行顺序:
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
与事件循环的交互
- 遇到
await
:async
函数的执行暂停。 - 释放线程:控制权返回给事件循环,允许运行其他任务。
- 等待:事件循环等待Promise解决。
- 恢复执行:一旦Promise解决,事件循环将异步函数剩余的部分放回任务队列中,从
await
暂停的地方恢复执行。
错误处理
try...catch
:在async
函数内部使用try...catch
结构来捕获异常,是处理错误的直接方式。
这条很好理解,因为不捕获异常的后果就是,后续代码不会再继续执行就被中断了,当然也可以在async
函数返回的Promise调用链的末尾使用.catch()
方法来处理错误。
并行执行
Promise.all()
:当有多个互不依赖的异步操作时,使用Promise.all()
并行执行这些操作,以节省时间。
这里一定要注意,还是以上面的代码为例,我再次使用了await等待两个异步操作,但发现实际上这几个操作是互不依赖的,这样的等待完全是徒劳的,我们可以使用Promise.all()来处理这样的情况。
async function exaFunc() {
console.log('1. 开始执行异步函数');
// await new Promise(resolve => setTimeout(resolve, 0)); // 这里模拟一个异步操作1
// await new Promise(resolve => setTimeout(resolve, 1000)); // 这里模拟一个异步操作2
// await new Promise(resolve => setTimeout(resolve, 2000)); // 这里模拟一个异步操作3
const result1 = new Promise(resolve => setTimeout(resolve, 0));
const result2 = new Promise(resolve => setTimeout(resolve, 0));
const result3 = new Promise(resolve => setTimeout(resolve, 0));
const results = await Promise.all([result1,result2,result3])
console.log('3. 异步操作完成');
}
console.log('0. 脚本开始');
exaFunc();
console.log('2. 异步函数已调用,继续执行脚本中的其他任务');
避免无谓的等待
- 不要滥用
await
,对于不依赖前一个异步操作结果的代码,不必使用await
等待。
await
在循环中的使用
- 尽量避免在循环中使用
await
等待异步操作,这会导致异步操作串行执行,降低程序效率。如果可能,考虑并行处理。
函数签名
- 使用
async
关键字的函数会返回一个Promise。即使函数内部直接返回非Promise值,该值也会被包装成一个已解决(resolved)的Promise。比如await 0,实际上就是promise.resolve(0)
结论
语法糖虽好,但还是不能乱吃,要按照基本法吃。