错误的理解引起的bug async await 执行顺序
今天有幸好碰到一个bug,让我知道了之前我对await async 的理解有点偏差。
错误的理解
之前我一直以为 await 后面的表达式,如果是直接返回一个具体的值就不会等待,而是继续执行async function 中的函数, 如下demo:
method () { getParams () { let params = {} if (this.serachFrom.time !== 0) { params.month = this.serachFrom.time.substr(5, 2) params.year = this.serachFrom.time.substring(0, 4) } return params }, async testNoAwait () { console.log('run testNoAwait') return 'this is no await' }, async testAsync () { console.log('run testAsync') let params = this.getParams() const data = await this.$store.dispatch('initSchemeTimeTest', params) return data }, async test () { console.log('test start') const v1 = await this.testNoAwait() console.log(v1) const v2 = await this.testAsync() console.log(v2) console.log(v1, v2) } }, created () { console.log('this is run created ') this.test() console.log('test last ...') console.log('test end ...') }
如上程序我之前认为 await this.testNoAwait() 会直接执行完不会等待,继续执行 console.log(v1),如果这样那么是一个错误的理解。
实际上MDN描述的暂停执行,并不是真正的暂停,而是让出了线程(跳出async函数体)然后继续执行后面的语句。
完整 demo code
vue created
async created () { console.log('this is run created ') this.test() // let data = this.test() // console.log(data) console.log('test last ...') console.log('test end ...') this.testSayHello() }
vue methods
testSayHello () { console.log('this is run hello') }, getParams () { let params = {} if (this.serachFrom.time !== 0) { params.month = this.serachFrom.time.substr(5, 2) params.year = this.serachFrom.time.substring(0, 4) } return params }, testNoAwait () { console.log('run testNoAwait') return 'this is no await' }, async testAsync () { console.log('run testAsync') let params = this.getParams() const data = await this.$store.dispatch('initSchemeTimeTest', params) return data }, async test () { console.log('test start') const v1 = await this.testNoAwait() console.log(v1) const v2 = await this.testAsync() console.log(v2) console.log(v1, v2) }
vuex 中
// actions
async initSchemeTimeTest ({commit, state, dispatch}, params) { console.log('run initSchemeTimeTest') const data = await schemeListTest(params) console.log('开始返回结果') commit(types.SCHEME_DATA_TIME_LIST, data) return data }
services api 中
注意在 testAsync 中 dispatch 了 initSchemeTimeTest,然后在调用了服务端的 schemeListTest
export async function schemeListTest (params) { console.log('this is run server') const data = await postTest(`/provid/spot/dailydeclareschemestatus/list`, params) return data }
common 中封装的 axiosServer
export function postTest (url, params) { return new Promise(async (resolve, reject) => { try { console.log('this is run common') const { data: { respHeader, respBody } } = await axiosServer({ url, type: 'post', params: { reqBody: params } }) if (respHeader.needLogin && process.env.NODE_ENV !== 'development') { Message.error(respHeader.message) location.href = condition.frontDomain + `/login?redirect=${encodeURI(condition.frontDomain + '/spot/race')}` reject(respHeader.message) } if (respHeader.resultCode === 0) { resolve(respBody || respHeader.message) } else { if (respHeader.resultCode === 21050 && respBody) { Message.error(respHeader.message) resolve(respBody) } else if (respHeader.message === '您没有该应用的权限') { location.href = 'frame.huidiancloud.com' } else { Message.error(respHeader.message) reject(respHeader.message) } } } catch (e) { reject(e) Message.error('系统繁忙,请稍后再试!') } }) }
如果按照之前的理解那么这个应该是输出了 run testNoAwait 之后继续输出 this is no await 。
控制台运行结果:
执行顺序
js是单线程(同时只能干一件事情),
以上测试的关键点在于当程序碰到await 时,把后面的表达式执行一次,然后把resolve 函数或者reject 函数(await 操作符会把表达式的结果解析成promise 对象) push 回调队列,接着跳过当前这个async function ,执行async function 后面的代码,如上面代码中,执行 this.testNoAwait() 之后就跳过 this.test()这个方法,执行了
console.log('test last ...') console.log('test end ...') this.testSayHello()
至于什么时候知道这个promise 对象的状态,这就是事件循环的事情了,监听到这个异步的状态事件改变时,如果执行环境栈是空的那么就会执行取出回调队列中的回调,推入执行环境栈,然后继续async function 后面的语句。
vue 开始执行created 生命周期
输出:this is run created
输出:test start
执行:testNoAwait // 关键
输出 :run testNoAwait 之后 跳过 test() 函数 执行created 后面的语句
输出:test last ... 、test end ... 、this is run hello
程序回到
const v1 = await this.testNoAwait()
如果监听到这个异步事件完成 则开始执行 后面的代码所以会
输出:this is no await
下面这个 await 跟上面同理
const v2 = await this.testAsync()
await 后面的表达式执行一次,如果里面存在await 也是同理继续执行下去,执行完之后,跳过这个async function 等到异步操作完成了继续回到 const v2 这里执行。
这里需要注意的是在common 中的postTest 中构造的Promise 对象是立即执行传入的function 所以在 services api 输出了 this is run server 之后接着输出 this is run common
因为上面的列子不是很方便看,所以我写了一个简单的测试 :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> <meta content="yes" name="apple-mobile-web-app-capable"> <meta content="black" name="apple-mobile-web-app-status-bar-style"> <meta content="telephone=no,email=no" name="format-detection"> <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>async await demo</title> </head> <body> <h1>async await demo</h1> </body> <script> async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') await async3() } async function async3() { console.log('async3') await async4() console.log('async4 end') } async function async4() { return new Promise(function (resolve, reject) { console.log('async4') resolve() }) } console.log('script start') setTimeout(function () { console.log('setTimeout') }, 0) async1(); new Promise(function (resolve) { console.log('promise1') resolve(); }).then(function () { console.log('promise2') }) console.log('script end') // script start async1 start async2 async3 async4 promise1 script end promise2 async4 end async1 end setTimeout </script> </html>
async awiat 执行顺序关键点
- 事件循环机制
- 回调队列
- 执行环境栈、入栈、出栈
- Promise 的构造函数是立即执行,但是他的成功、失败的回调函数是一个异步执行的回调
- Promise 的回调优先于 setTimeout 的任务队列
- async 返回promise 对象
- await 表达式的作用和返回值
总结
1、js 是单线程(同时只能做一件事情),在js引擎内部异步的处理是跟事件循环机制、以及回调队列有关
2、构造的promise 对象是立即执行传入的function
3、async function 是返回一个promise 对象
4、await 操作符会把表达式的结果进行解析成promise 对象