本文谈一下我对async/await在相对于promise,setTimeout和同步事件在事件循环中的执行顺序的个人理解,网上铺垫盖地的async await执行顺序的博客有很多,但有些说的并不是特别详细,甚至连宏任务,微任务都没有提及,本人在看过那些博客后针对一些先前不理解的点进行分析
写在前面
1 本文针对async/await在事件循环中的执行顺序,但不会详细的讲事件循环,读者需要理解什么是宏任务,微任务,事件循环,关于事件循环可参考这篇博客微任务、宏任务与Event-Loop
2 本人入行不久,写这篇博客主要目的是探讨交流,有一些个人的理解,如有差异欢迎指出,希望对有同样问题的人有所帮助
async await是什么
async函数
MDN上是这么解释的
**async function**声明用于定义一个返回AsyncFunction对象的异步函数异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的Promise返回其结果
如果没有明白我们可以打印一个async函数,看看它到底是什么
async function asyncFn() {
console.log('1 am async function')
}
console.log(asyncFn())可以看到asyncFn和正常的函数一样执行函数,输出了一句l am async function,一般的函数不同的时它返回了一个Promise对象,并且Promise对象的状态值是resolved,而resolve的值是undefined
再来看一个有return值的async函数
async function asyncFn() {
console.log('1 am async function')
return 'l have been returned'
}
console.log(asyncFn())这时asyncFN函数和之前一样返回了一个Promise对象,并且状态是resolved,和之前不同的是它resolve的值是函数return的值
换句话说,async函数会把返回值包装成一个状态为resolved的Promise对象,且Promise对象的值为该函数原本返回的值(这里不考虑rejected的情况)
await关键字
await关键字只能在async函数中才能使用,用于等待一个await后面的表达式的执行结果
await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈的代码。等本轮的宏任务(执行顺序优于本轮微任务)执行完了之后又会跳回到async函数中等待await
await后面大多跟着Promise对象,但是同样可以是一个普通值
- await后面跟着Promise对象时,它会等待Promise对象的状态变为resolve或者reject,之后会将这个Promise对象resolve或者reject中携带的值放入微任务队列中
2.await后面跟着普通值的时候会直接返回这个值
这里还有一点需要补充也是我之前一直搞不懂async/await执行顺序的原因之一,关于Promise对象,它有3种状态,pending(等待),resolved(完成),rejected(拒绝),而resolved和rejected状态如果你不调用对应的then()/catch()方法,它是不会返回值的,依然还是一个Promise对象,直到你调用then()/catch(),而这2个方法会将Promise的值放入微任务队列中,才能在微任务队列中拿到这个返回的值
而await关键字和then()类似,await表达式的值等于状态已经是resolved的Promise对象返回的值,await和then方法一样,有个关键的操作就是把resolved放到微任务队列中,来看例子
function testPromise() {
return new Promise(resolve => {
resolve('promise')
})
}
async function asyncFn() {
let res = await testPromise()
console.log(res)
}
asyncFn()
console.log('script end')这里我声明了一个testPromise函数,执行这个函数会返回一个Promise对象,在asyncFn函数中,所以在asyncFn函数中,await后面跟着的是一个Promise对象,我来分析一下这2个console的打印顺序
- 首先asyncFn(),执行asyncFn函数,遇到await关键字会执行后面的testPromise函数
- testPromise执行结束后先不会return,而是跳出async函数(让出线程)执行之后的语句
- 接下来打印script end,至此第一轮事件循环的宏任务结束
- 根据await的特性,再次跳转到async里面让await后面的表达式返回结果,而await后面表达式也就是testPromise函数返回了一个状态为resolved,值为"promise"的Promise对象(注意此时并没有从Promise对象中获得"promise"字符串)
- 因为testPromise返回了一个Promise对象,await会尝试将这个Promise的值拿出来,也就是将"promise"字符串作为回调放到第一轮事件循环的微任务队列里,从微任务队列中可以获取到"promise"字符串
- 在第5步的时候,第一轮事件循环宏任务已经结束,但是微任务还在进行,把"promise"字符放到微任务队列中后,js会查看微任务队列中是否还有任务,如果有则继续停留在第一轮微任务的执行过程,然后处理微任务队列中的微任务,直到所有在这一轮微任务队列中的任务全部完成,第一轮事件循环才算真正结束,这时候才开始第二轮事件循环的宏任务队列("如果在处理微任务的过程中不停的添加微任务同样会导致阻塞,会一直停留在当轮事件循环,并不会开始下一轮")
这里再用一个相对完整的例子来解释一下5,6步到底是什么意思
例子
async function asyncFn() {
console.log('asyncFn')
let res = await asyncFn2()
console.log(res)
}
async function asyncFn2() {
console.log('asyncFn2')
let res = await fn3()
console.log(res)
return 'asyncFn2 return'
}
function fn3() {
console.log('fn3')
}
setTimeout(() => {
console.log('setTimeout')
}, 0)
console.log('script start')
asyncFn()
let promise = new Promise(resolve => {
console.log('promise')
resolve('promise resolved')
console.log('after promise resolved')
}).then(res => {
console.log(res)
})
console.log('script end')控制台运行结果如下:
我们来逐一分析一下
- 最开始script标签作为第一轮宏任务开始
- 遇到setTimeout,setTimeout是宏任务,将它放在宏任务队列中
- 打印"script start"
- 执行asyncFn(),打印"asyncFn"
- 遇到await关键字,先执行await关键字后面的asyncFn2函数,打印"asyncFn2",再次遇到await关键字执行fn3函数打印"fn3"
- 打印完"fn3"后会跳出最外层的async函数(也就是跳出asyncFn函数)
- 新建Promise对象,打印"promise",遇到resolve方法,将"promise resolved"字符串放入当前事件循环的微任务队列中,并且将Promise对象的状态修改为resolved(再次说明一下,Promise对象状态为resolved/rejected还需要用then()/catch()将Promise对象的值从微任务队列中取出来才算是获得该Promise对象的值),随后打印"after promise resolved"
- 打印"script end",至此第一轮事件循环的宏任务结束
此时微任务队列有一个任务
宏任务队列有一个任务
9.在宏任务结束后又会跳会async函数中(指的是asyncFn2函数),因为fn3并不是一个async函数,不会返回一个Promise对象,所以await一个没有return值的普通函数会返回undefined,随后打印res,即undefined
- asyncFn2函数返回"asyncFn2 return"字符串,而这个字符串因为async函数,会被包装成一个状态为resolved的Promise对象
- 因为asyncFn2函数返回了Promise对象,接着跳转到asyncFn函数,asyncFn函数中的那个await关键字会尝试将asyncFn2函数的返回值拿出来,也就是说通过将第10步的那个状态为resolved值为"asyncFn2 return"的Promise对象放到微任务队列中,从微任务队列取出这个Promise对象的值
此时微任务队列有两个任务
宏任务队列有一个任务
- 而11的操作又会跳出async函数,此时程序开始处理微任务队列,发现微任务队列中有第7步Promise对象的resolved的值,随即执行第7步then方法后面的回调,打印"promise resolved"
13 .随后发现微任务队列中还有一个微任务,即11步放入的微任务,随后执行回调,打印"asyncFn2 return",至此,微任务队列中没有任务了,第一轮事件循环的宏任务和微任务结束
此时宏任务队列有一个任务
14.开始第二轮事件循环,从宏任务开始,发现有之前setTimeout宏任务放入的回调,执行回调打印"setTimeout"
分析部分有点长,建议保存图片对比查看,主要考察了开发者的事件循环,promise,异步这些知识点,如果其中有错误,请务必指出,在第一时间修改
感谢观看