1. 首页

当Async or Await的遇到了EventLoop

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 FunctionsPromiseResolveThenableJob的文档说明,简单来讲:浏览器在解析代码: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() )
})

就是一个“状态跟随”。

转换依据:

当Async or Await的遇到了EventLoop

当Async or Await的遇到了EventLoop

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,因此,就不会有额外的微任务产生。

抓换依据:

当Async or Await的遇到了EventLoop

当Async or Await的遇到了EventLoop

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

期待点赞;不足之处,欢迎指出。

推荐系列教程

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:当Async or Await的遇到了EventLoop

链接:https://www.javascriptc.com/3676.html

« 请问如何实现一个轮播图组件
阿里淘外商业化广告工程架构实践»
Flutter 中文教程资源

相关推荐

QR code