ES的“异步处理”发展到了今天,已经出现了相对成熟的方案了:Async/Await。在这份方案中,被关键词:async修饰的函数将返回一个promise,并且可以被await调用。
那从EventLoop的角度去理解,async/await是一个怎样的执行过程呢?
本文旨意:
1、探究Async函数转化为普通函数的等价形式
2、探究async/await在EventLoop中的执行机制
申明:
1、如果特殊申明,本文中的代码均运行在Chrome v73以上版本(例如笔者当前安装的版本:v77)
一、一道面试题
二、转换对比
三、PromiseResolveThenableJob
四、案例分析
五、Await转换
六、总结
本文案例涉及到的一些必备的知识点:
1、ES中涉及到Async/Await的异步知识
2、Promise的相关知识点
3、浏览器中的EventLoop(事件循环)
4、Thenable对象与Promise,EventLoop中的Promise
5、new Promoise((rs, rj)=>{rs( otherPromise )})涉及到的异步Job
希望读者对前面4个要点有一定的了解甚至掌握,这样才能更方便读者阅读本文
第5点涉及到的Job其实就是PromiseResolveThenableJob,会在文末提供更详细的文档资料
文章内容难度:★★★
一、一道面试题
为了方便引用,我们把它命名为:【1.no-return-statement】
1、原始版本(版本一):no-return-statement
{
async function async1(){
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2(){
console.log( 'async2' )
}
setTimeout(()=>{
console.log( 'setTimeout' )
}, 0)
async1()
new Promise(( resolve )=>{
console.log( 'promise1' )
resolve()
}).then(()=>{
console.log( 'promise2' )
}).then(()=>{
console.log( 'promise3' )
}).then(()=>{
console.log( 'promise4' )
})
}
此段代码在Chrome v77中的输出是:
async1 start
async2
promise1
async1 end
promise2
promise3
promise4
setTimeout
码农进阶题库 – 每天一道面试题 or Js小知识@Js中文网
每天弄懂一道面试题(Javascript小知识)来鞭策自己学习思考,每天进步一点,帮助你提高你的代码编写质量
这个输出其实并没有违法直觉的地方,是一个很符合逻辑的输出。
如果读者对于这一段的输出还有困惑,建议先了解一下EventLoop以及Promise在EventLoop的运行原理。 之所以强调Chrome版本,是因为【1.no-return-statement】在低于v72版本的Chrome又是另一番输出,感兴趣的读者可以自行测试一下。
为了方便演示,我们把最后一个promise的代码修改为:
new Promise(( resolve )=>{
console.log( 'promise' )
resolve()
}).then(()=>{
console.log(`%c promise.then1`,`color: blue;`)
}).then(()=>{
console.log(`%c promise.then2`,`color: blue;`)
}).then(()=>{
console.log( 'promise.then3' )
}).then(()=>{
console.log( 'promise.then4' )
}).then(()=>{
console.log( 'promise.then5' )
})
二、转换对比
此章节强烈建议配合Git代码一起看(文末有Git地址)
1、修改版本:return-undefined
按照Async的规范,被async修饰的函数将返回一个promise,即便在函数体内手动return一个原始值,亦被包装为一个promise。
于是,我们有了第二个版本,命名为:【2.return-undefined】:
async function async2(){
console.log( 'async2' )
return undefined
}
此段代码在Chrome v77中的输出与【1.no-return-statement】:
async1 start
async2
promise
async1 end
promise.then1
promise.then2
promise.then3
promise.then4
promise.then5
setTimeout
读者将文末的git项目clone到本地,把对应的HTML文件放在Chrome中打开即可。
2、修改版本:async.return-Promise.resolve 与 async.return-new.Promise
如果我们手动return一个promise,会怎样呢?
得到一个resolve的Promise,可以是:new Promise((resolve, reject)=>{resolve()}),也可以是:Promise.resolve(),我们分别试一下。
a. 返回Promise.resolve:
于是,我们有了第三个版本,命名为:【3.async.return-Promise.resolve】:
async function async2(){
console.log( 'async2' )
return Promise.resolve( undefined )
}
此段代码在Chrome v77中的输出是:
async1 start
async2
promise
promise.then1
promise.then2
async1 end
promise.then3
promise.then4
promise.then5
setTimeout
码农进阶题库 – 每天一道面试题 or Js小知识@Js中文网
每天弄懂一道面试题(Javascript小知识)来鞭策自己学习思考,每天进步一点,帮助你提高你的代码编写质量
与版本一【1.no-return-statement】或者版本二【2.return-undefined】不同的是,“async1 end”输出滞后两个时序,被放在了“promise.then2”后面。文末的git项目提供了此版本的代码。
很明显,运行情形已经出现变化了。
b. 返回new Promise:
这样,我们有了第四个版本,命名为:【4.async.return-new.Promise】:
async function async2(){
console.log( 'async2 @ return-Promise.resolve' )
return new Promise((resolve, reject)=>{resolve( undefined )})
}
此段代码在Chrome v77中的输出与【3.async.return-Promise.resolve】无异,读者将文末的git项目clone到本地,把对应的HTML文件放在Chrome中打开即可。
async1 start
async2
promise
promise.then1
promise.then2
async1 end
promise.then3
promise.then4
promise.then5
setTimeout
其实到这里已经可以说明一个问题了,那就是在异步函数中,手动返回一个原始类型的值和手动返回一个promise是有区别的,具体表现在EventLoop的输出结果上,所以,也就说明了两者在执行时,microtask队列上是有区别的。
3、修改版本:去除async关键词
我们将async2去除async关键词,修改为一个普通的函数。同时,我们新建一个中间函数:asyncMiddle函数,用来返回两种形式的promise。
结合版本三与版本四,我们可以得到四个版本的代码:
版本五:【5.return-Promise.resolve-return-Promise.resolve】:
function asyncMiddle(){
return Promise.resolve( async2() )
}
function async2(){
console.log( 'async2 @ return-Promise.resolve' )
return Promise.resolve( undefined )
}
输出:
async1 start
async2
promise
async1 end
promise.then1
promise.then2
promise.then3
promise.then4
promise.then5
setTimeout
版本六:【6.return-Promise.resolve-return-new.Promise】:
function asyncMiddle(){
return Promise.resolve( async2() )
}
function async2(){
console.log( 'async2' )
return new Promise((resolve, reject)=>{resolve( undefined )})
}
输出:
async1 start
async2
promise
async1 end
promise.then1
promise.then2
promise.then3
promise.then4
promise.then5
setTimeout
版本七:【7.return-new.Promise-return-Promise.resolve】:
function asyncMiddle(){
return new Promise((resolve, reject)=>{resolve( async2() )})
}
function async2(){
console.log( 'async2' )
return Promise.resolve( undefined )
}
输出:
async1 start
async2
promise
promise.then1
promise.then2
async1 end
promise.then3
promise.then4
promise.then5
setTimeout
版本八:【8.return-new.Promise-return-new.Promise】:
function asyncMiddle(){
return new Promise((resolve, reject)=>{resolve( async2() )})
}
function async2(){
console.log( 'async2' )
return new Promise((resolve, reject)=>{resolve( undefined )})
}
输出:
async1 start
async2
promise
promise.then1
promise.then2
async1 end
promise.then3
promise.then4
promise.then5
setTimeout
版本七与版本八的表现,与版本三和版本四的输出表现一致,因此,也可以说:
async function fn(){
return <Promise>
}
在运行上,可以等价于:
function _fn(){
return new Promise((resolve, reject)=>{resolve( <Promise> )})
}
到这里,本文旨意中的第一个已经探究出来了。
需要说明的是,这个结论,仅仅是实验性的结论。
另有文章也支持等价于这种形式:
function fn(){
return Promise.resolve( <Promise> )
}
但这个结论无法解释上述版本之间的输出区别,因此,笔者在此保留意见,仍以本文得出的结论为主。
三、PromiseResolveThenableJob
我们以相同的输出,反推了Async函数的等价形式,但,为什么版本七和版本八会延迟两个时序输出呢?
或者说:new Promise((rs, rj)=>{rs( )})对比Promise.resolve( ),在微任务的具体执行中有着怎样差别。
1、Thenable对象
若一个对象或其原型上具有then方法,那么即可称该对象为一个Thenable对象。
例如:
let thenableObj = {
then(rs, rj){
//
}
}
因此,一个Promise对象亦可以称为一个Thenable对象。
依据TC39对Promise Resolve Functions和PromiseResolveThenableJob的文档说明,简单来讲:浏览器在解析代码:new Promise((rs, rj)=>{rs( )}) 时,会创建一个“PromiseResolveThenableJob”的微任务。
那具体是怎样的呢?
2、状态跟随
对于new Promise((rs, rj)=>{rs( )}),我们也可以称:newPromise的状态是跟随otherPromise的,简称“状态跟随”。
【状态跟随1】代码如下:
const promiseA = new Promise((resolve, reject)=>{
resolve( 'ccc' )
})
const promiseB = new Promise((resolve, reject)=>{
resolve( promiseA )
})
promiseB.then(()=>{
console.log( 'promiseB then' )
})
promiseA.then(()=>{
console.log( 'promiseA then' )
})
Promise.resolve().then(()=>{
console.log( 'p.then1' )
}).then(()=>{
console.log( 'p.then2' )
}).then(()=>{
console.log( 'p.then3' )
}).then(()=>{
console.log( 'p.then4' )
}).then(()=>{
console.log( 'p.then5' )
}).then(()=>{
console.log( 'p.then6' )
})
输出:
promiseA then
p.then1
p.then2
promiseB then
p.then3
p.then4
p.then5
p.then6
解析:
浏览器在解析promiseB的时候,发现其new Promise中resolve的是另一个Thenable对象(另一个实例Promise),会创建一个PromiseResolveThenableJob的微任务来完成转换工作,等到promiseA被resolved之后,promiseB才会被resolved。
PromiseResolveThenableJob大致会生成如下的伪代码:
promiseA.then(()=>{
resolvePromiseB,
rejectPromiseB
})
微任务解析:
步骤1:
执行:执行main stack,生成PromiseResolveThenableJob1微任务,生成console.log( 'promiseA then' )微任务、console.log( 'p.then1' )微任务
微任务队列:PromiseResolveThenableJob1、console.log( 'promiseA then' )、console.log( 'p.then1' )
输出:<无>
步骤2:
执行:执行微任务PromiseResolveThenableJob1,生成resolvePromiseB微任务
微任务队列:console.log( 'promiseA then' )、console.log( 'p.then1' )、resolvePromiseB
输出:<无>
步骤3:
执行:执行微任务console.log( 'promiseA then' )
微任务队列:console.log( 'p.then1' )、resolvePromiseB
输出:promiseA then
步骤4:
执行:执行微任务console.log( 'p.then1' ),生成console.log( 'p.then2' )回调微任务
微任务队列:resolvePromiseB、console.log( 'p.then2' )
输出:p.then1
步骤5:
执行:执行微任务resolvePromiseB(之后完后promiseB才算是真正地被resolved),生成console.log( 'promiseB then' )
微任务队列:console.log( 'p.then2' )、console.log( 'promiseB then' )
输出:<无>
步骤6:
执行:执行微任务console.log( 'p.then2' ),生成console.log( 'p.then3' )回调微任务
微任务队列:console.log( 'promiseB then' )、console.log( 'p.then3' )
输出:p.then2
步骤7:
执行:执行微任务console.log( 'promiseB then' )
微任务队列:console.log( 'p.then3' )
输出:promiseB then
步骤8:
执行:执行微任务console.log( 'p.then3' ),生成console.log( 'p.then4' )回调微任务
微任务队列:console.log( 'p.then4' )
输出:p.then3
步骤......
针对这个过程分析可以总结出两点:
a. 浏览器解析类似new Promise((rs, rj)=>{rs( )})的代码时,会在微任务中插入一个PromiseResolveThenableJob微任务。如果被追随的Promise的状态被resolved,会立即插入追随者的resolvePromise微任务。正因为这两个微任务的存在,才导致了对应的输出延迟了两个时序。
b. 对于链式的then回调,浏览器只有在执行完毕上一个then回调后,才会把当前then的回调添加到微任务队列末尾。
3、Promise.resolve( )
这里直接说一个结论:按照TC39规范,Promise.resolve( )的执行,会直接返回,不会像额外的生成Job微任务。
上述的各个版本之间的对比,其实也是这个结论的一个佐证,读者可以细细品味。
四、案例分析
1、重新看待版本七和版本八
如果我们再回头重新观察版本七和版本八,其实它们中也各包含一个“状态追随”的实例在里面。
以版本八为例,版本八代码:
function asyncMiddle(){
return new Promise((resolve, reject)=>{resolve( async2() )})
}
function async2(){
console.log( 'async2' )
return new Promise((resolve, reject)=>{resolve( undefined )})
}
可以为认为:asyncMiddle追随async2的状态。
因此,对应的PromiseResolveThenableJob伪代码如下:
async2.then(()=>{
resolvePromiseAsyncMiddlePromise,
rejectPromiseAsyncMiddlePromise
})
微任务分析(我们暂且把setTimeout这个macrotask忽略掉):
步骤1:
执行:执行main stack,生成PromiseResolveThenableJob1微任务,生成console.log(`%c promise.then1`,`color: blue;`)回调微任务
微任务队列:PromiseResolveThenableJob1、console.log(`%c promise.then1`,`color: blue;`)
输出:async1 start、async2、promise(如果读者对这一步的输出仍然存在疑惑,可以再复习一下EventLoop)
步骤2:
执行:执行微任务PromiseResolveThenableJob1,生成resolvePromiseAsyncMiddlePromise微任务
微任务队列:console.log(`%c promise.then1`,`color: blue;`)、resolvePromiseAsyncMiddlePromise
输出:<无>
步骤3::
执行:执行微任务console.log(`%c promise.then1`,`color: blue;`),生成console.log(`%c promise.then2`,`color: blue;`)回调微任务
微任务队列:resolvePromiseAsyncMiddlePromise、console.log(`%c promise.then2`,`color: blue;`)
输出: promise.then1
步骤4:
执行:执行微任务resolvePromiseAsyncMiddlePromise,生成console.log(`%c async1 end`,`color: red;`)回调微任务
微任务队列:console.log(`%c promise.then2`,`color: blue;`)、console.log(`%c async1 end`,`color: red;`)
输出:<无>
步骤5:
执行:执行微任务console.log(`%c promise.then2`,`color: blue;`),生成console.log( 'promise.then3' )回调微任务
微任务队列:console.log(`%c async1 end`,`color: red;`)、console.log( 'promise.then3' )
输出: promise.then2
步骤5:
执行:执行微任务console.log(`%c async1 end`,`color: red;`)
微任务队列:console.log( 'promise.then3' )
输出: async1 end
步骤......
五、Await转换
await指令后面可以是一个原始值,也可以是一个Promise对象,同样,可以将await转换成promise语法。
仅从浏览器平台角度考虑,这里需要区分Chrome v73以上版本的实现,和v73以下版本的实现(因为前后的实现确实有区别)。
以Chrome v63版本为基础,上述版本一的代码:
async function async1(){
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
会被转换成:
function _async1(){
console.log( 'async1 start' )
let implicit_promise = new Promise((resolve, reject)=>{
let promise = new Promise((rs, rj)=>{
rs( async2() )
})
promise.then(()=>{
console.log( 'async1 end' )
resolve()
})
})
return implicit_promise
}
其中的:
let promise = new Promise((rs, rj)=>{
rs( async2() )
})
就是一个“状态跟随”。
转换依据:
V8对await的抓换处理(旧版实现)
在Chrome v63上的输出为:
async1 start
async2
promise1
promise2
promise3
async1 end
promise4
setTimeout
对比在Chrome v77上的输出,可以看到,延迟了两个时序。
而在Chrome v77(v73以上版本)上,版本一被抓换为:
function _async1(){
console.log( 'async1 start' )
let implicit_promise = new Promise((resolve, reject)=>{
let promise = Promise.resolve( async2() )
promise.then(()=>{
console.log( 'async1 end' )
resolve()
})
})
return implicit_promise
}
依据上述的论述,由let定义的promise可以被看成是async2返回的promise,因此,就不会有额外的微任务产生。
抓换依据:
V8对await的抓换处理(新版实现)
额外地,如果是“链式”的“状态跟随”会怎样?
例如,【状态跟随2】:
const promiseA = new Promise((resolve, reject)=>{
resolve( 'ccc' )
})
const promiseB = new Promise((resolve, reject)=>{
resolve( promiseA )
})
const promiseC = new Promise((resolve, reject)=>{
resolve( promiseB )
})
const promiseD = new Promise((resolve, reject)=>{
resolve( promiseC )
})
promiseD.then(()=>{
console.log( 'promiseD then' )
})
promiseC.then(()=>{
console.log( 'promiseC then' )
})
promiseB.then(()=>{
console.log( 'promiseB then' )
})
promiseA.then(()=>{
console.log( 'promiseA then' )
})
Promise.resolve().then(()=>{
console.log( 'p.then1' )
}).then(()=>{
console.log( 'p.then2' )
}).then(()=>{
console.log( 'p.then3' )
}).then(()=>{
console.log( 'p.then4' )
}).then(()=>{
console.log( 'p.then5' )
}).then(()=>{
console.log( 'p.then6' )
})
读者可以git clone本文的所有代码,尝试在本地运行,如果不太理解输出,代码中也会有微任务步骤分析。
至此,本文旨意中的第二个已经论述得差不多了,希望能对大家有所帮助。
六、总结
本文主要论述了两点内容:
1、async函数转换成普通函数时,该怎样的手动返回promise。依据对照试验结果,应当是返回new Promise式的promise。
2、new Promise在处理一个另一个promise时与Promise.resolve的区别。
3、await转换成promise的结果以及新旧两个版本的结果区别,并因此导致的输出执行差异。
本例的github:thesis-asyncfunc-return
期待点赞;不足之处,欢迎指出。
推荐系列教程
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com