这就是编程:async/await原来就这?
前言
“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”
Hello,《这就是编程》每周六晚如期更新,带大家走进编程世界,一起读懂源码,读懂编程!
经过上一期《这就是编程:你知道在cxx语言层面Promise如何实现吗》,已经对JavaScript异步编程有了进一步的认识。
Error-First Callback风格的异步代码容易出现Callback Hell,Promise的出现某种意义上解决了这个问题,但是期待一种同步风格来编写异步代码。
随着JavaScript语言特性开始支持async function,async/await可以达到同步代码风格,这种“同步”方式是如何实现的呢?今天第二期,带来关于在cxx层面async function实现原理的源码分析,让你进一步提升对JavaScript异步编程的认识,以及掌握async/await使用细节。
假如读者想对Promise实现原理做进一步了解,可以查看上一期内容👆🏻,因为Promise是async/await的基础。
OK,Let's start
执行过程
JavaScript是解释型语言,如上图,V8经过AST parser和Bytecode-Generator得到JavaScript与机器码的中间产物bytecode,bytecode展示了JavaScript执行过程。
全文以下面这段代码为例,分析在V8中async function如何执行。
async function foo(v) {
const w = await v;
return w;
}
foo(42)
.then((res) => { console.log(res) })
.catch((err) => { console.log(err) })
首先看一下foo函数声明时生成的bytecode
[generated bytecode for function: (0x0efd0025a71d <SharedFunctionInfo>)]
Bytecode length: 56
Parameter count 1
Register count 5
Frame size 40
Bytecode age: 0
0xefd0025a85e @ 0 : 13 00 LdaConstant [0]
0xefd0025a860 @ 2 : c3 Star1
0xefd0025a861 @ 3 : 19 fe f8 Mov <closure>, r2
0xefd0025a864 @ 6 : 65 64 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
0xefd0025a869 @ 11 : 21 01 00 LdaGlobal [1], [0]
0xefd0025a86c @ 14 : c1 Star3
0xefd0025a86d @ 15 : 0d 2a LdaSmi [42]
0xefd0025a86f @ 17 : c0 Star4
0xefd0025a870 @ 18 : 62 f7 f6 02 CallUndefinedReceiver1 r3, r4, [2]
0xefd0025a874 @ 22 : c1 Star3
0xefd0025a875 @ 23 : 2d f7 02 04 GetNamedProperty r3, [2], [4]
0xefd0025a879 @ 27 : c2 Star2
0xefd0025a87a @ 28 : 80 03 00 00 CreateClosure [3], [0], #0
0xefd0025a87e @ 32 : c0 Star4
0xefd0025a87f @ 33 : 5e f8 f7 f6 06 CallProperty1 r2, r3, r4, [6]
0xefd0025a884 @ 38 : c2 Star2
0xefd0025a885 @ 39 : 2d f8 04 08 GetNamedProperty r2, [4], [8]
0xefd0025a889 @ 43 : c3 Star1
0xefd0025a88a @ 44 : 80 05 01 00 CreateClosure [5], [1], #0
0xefd0025a88e @ 48 : c1 Star3
0xefd0025a88f @ 49 : 5e f9 f8 f7 0a CallProperty1 r1, r2, r3, [10]
0xefd0025a894 @ 54 : c4 Star0
0xefd0025a895 @ 55 : a9 Return
Constant pool (size = 6)
0xefd0025a81d: [FixedArray] in OldSpace
- map: 0x0efd00000089 <Map(FIXED_ARRAY_TYPE)>
- length: 6
0: 0x0efd0025a765 <FixedArray[2]>
1: 0x0efd0025a6d5 <String[3]: #foo>
2: 0x0efd0000605d <String[4]: #then>
3: 0x0efd0025a7ad <SharedFunctionInfo>
4: 0x0efd0020378d <String[5]: #catch>
5: 0x0efd0025a7e5 <SharedFunctionInfo>
Handler Table (size = 0)
Source Position Table (size = 0)
从bytecode中发现,async function的声明过程和普通函数一样,毕竟async function是以Function.prototype
为原型。
await实现原理
重点分析一下对应foo函数执行的bytecode,如下:
[generated bytecode for function: foo (0x0efd0025a775 <SharedFunctionInfo foo>)]
Bytecode length: 87
Parameter count 2
Register count 6
Frame size 48
Bytecode age: 0
0xefd0025a9fe @ 0 : ae fa 00 01 SwitchOnGeneratorState r0, [0], [1] { 0: @33 }
0xefd0025aa02 @ 4 : 19 fe f8 Mov <closure>, r2
0xefd0025aa05 @ 7 : 19 02 f7 Mov <this>, r3
0xefd0025aa08 @ 10 : 68 02 f8 02 InvokeIntrinsic [_AsyncFunctionEnter], r2-r3
0xefd0025aa0c @ 14 : c4 Star0
0xefd0025aa0d @ 15 : 19 ff f8 Mov <context>, r2
0xefd0025aa10 @ 18 : 19 fa f7 Mov r0, r3
0xefd0025aa13 @ 21 : 19 03 f6 Mov a0, r4
0xefd0025aa16 @ 24 : 68 01 f7 02 InvokeIntrinsic [_AsyncFunctionAwaitUncaught], r3-r4
0xefd0025aa1a @ 28 : af fa fa 03 00 SuspendGenerator r0, r0-r2, [0]
0xefd0025aa1f @ 33 : b0 fa fa 03 ResumeGenerator r0, r0-r2
0xefd0025aa23 @ 37 : c1 Star3
0xefd0025aa24 @ 38 : 68 0b fa 01 InvokeIntrinsic [_GeneratorGetResumeMode], r0-r0
0xefd0025aa28 @ 42 : c0 Star4
0xefd0025aa29 @ 43 : 0c LdaZero
0xefd0025aa2a @ 44 : 1c f6 TestReferenceEqual r4
0xefd0025aa2c @ 46 : 98 05 JumpIfTrue [5] (0xefd0025aa31 @ 51)
0xefd0025aa2e @ 48 : 0b f7 Ldar r3
0xefd0025aa30 @ 50 : a8 ReThrow
0xefd0025aa31 @ 51 : 19 f7 f9 Mov r3, r1
0xefd0025aa34 @ 54 : 19 fa f7 Mov r0, r3
0xefd0025aa37 @ 57 : 19 f9 f6 Mov r1, r4
0xefd0025aa3a @ 60 : 68 04 f7 02 InvokeIntrinsic [_AsyncFunctionResolve], r3-r4
0xefd0025aa3e @ 64 : a9 Return
0xefd0025aa3f @ 65 : c1 Star3
0xefd0025aa40 @ 66 : 82 f7 01 CreateCatchContext r3, [1]
0xefd0025aa43 @ 69 : c2 Star2
0xefd0025aa44 @ 70 : 10 LdaTheHole
0xefd0025aa45 @ 71 : a6 SetPendingMessage
0xefd0025aa46 @ 72 : 0b f8 Ldar r2
0xefd0025aa48 @ 74 : 1a f7 PushContext r3
0xefd0025aa4a @ 76 : 17 02 LdaImmutableCurrentContextSlot [2]
0xefd0025aa4c @ 78 : bf Star5
0xefd0025aa4d @ 79 : 19 fa f6 Mov r0, r4
0xefd0025aa50 @ 82 : 68 03 f6 02 InvokeIntrinsic [_AsyncFunctionReject], r4-r5
0xefd0025aa54 @ 86 : a9 Return
Constant pool (size = 2)
0xefd0025a9cd: [FixedArray] in OldSpace
- map: 0x0efd00000089 <Map(FIXED_ARRAY_TYPE)>
- length: 2
0: 33
1: 0x0efd0025a99d <ScopeInfo CATCH_SCOPE>
Handler Table (size = 16)
from to hdlr (prediction, data)
( 18, 65) -> 65 (prediction=3, data=2)
Source Position Table (size = 0)
对照如下执行示意图:
- 首先async function编译后被标记成
resumable
,也就是byecode中SwitchOnGeneratorState
,因为async/await底层是以Generator
为基础封装,也是借由Generator
的协程机制实现可挂起、可恢复 - 接着
InvokeIntrinsic [_AsyncFunctionEnter]
,async function开始执行,在[AsyncFunctionEnter](https://github.com/v8/v8/blob/main/src/builtins/builtins-async-function-gen.cc#L79)
里,核心逻辑包括计算寄存器及参数数量、创建寄存器、创建需返回调用者的promise实例以及创建代表async function自身的async_function_object
对象 - 然后
InvokeIntrinsic [_AsyncFunctionAwaitUncaught]
,即是进入await语法糖处理逻辑,以Promise实例包装每一个await point,并在handlers里封装resume/throw处理逻辑,以实现在特定执行时机(比如promise fulfilled)恢复且返回结果值
说到这里,先穿插分析下await语法糖的实现原理,V8在生成bytecode的过程中会经过很多的visitor,对于await关键字会进入BytecodeGenerator::VisitAwait
处理
在VisitAwait
里,对当前await point保存现场,然后对其进行BuildAwait
,使得每一个await point成为resumable
,即可挂起和可恢复,具体看看BuildAwait
是如何处理:
-
首先为register寄存器设置正确的作用域,因为这关系到await point在
LoadAccumulatorWithRegister
时能否从正确的register里获取结果值,即在JavaScript层面await能否获取到预期结果值 -
确定await关键字的
FunctionId
,这个FunctionId
直接决定了await在cxx层面的逻辑处理,即怎么处理跟随的“数据”。上面bytecode里调用的InvokeIntrinsic [_AsyncFunctionAwaitUncaught]
正是FunctionId
所指向的cxx内部AsyncFunctionBuiltinsAssembler
的built-in方法AsyncFunctionAwaitUncaught
0xefd0025aa16 @ 24 : 68 01 f7 02 InvokeIntrinsic [_AsyncFunctionAwaitUncaught], r3-r4
-
绑定
FunctionId
作为await的逻辑处理函数,然后获取register列表,并作为闭包参数传给await使用 -
接着
BuildSuspendPoint
为await point标记
suspend_id
构建suspend point,使得await point可被挂起。suspend_id
是suspend point的唯一索引,缓存在generator_jump_table_
中。等await触发时,在suspend_id
位置挂起async_function_object
;await处理完成时从suspend_id
对应await point恢复继续执行。0xefd0025aa1a @ 28 : af fa fa 03 00 SuspendGenerator r0, r0-r2, [0] 0xefd0025aa1f @ 33 : b0 fa fa 03 ResumeGenerator r0, r0-r2
-
最后判断当前
async_function_object
是否需要resume
,resume_mode
为true,表明async function需要从suspend_id
位置ResumeGenerator
恢复继续执行,则将async_function_object
的当前value返回给当前await point,进入InvokeIntrinsic [[_AsyncFunctionResolve](https://github.com/v8/v8/blob/main/src/builtins/builtins-async-function-gen.cc#L179)]
;否则ReThrow
抛错退出进入InvokeIntrinsic [[_AsyncFunctionReject](https://github.com/v8/v8/blob/main/src/builtins/builtins-async-function-gen.cc#L156)]
0xefd0025aa1f @ 33 : b0 fa fa 03 ResumeGenerator r0, r0-r2 0xefd0025aa23 @ 37 : c1 Star3 0xefd0025aa24 @ 38 : 68 0b fa 01 InvokeIntrinsic [_GeneratorGetResumeMode], r0-r0 0xefd0025aa28 @ 42 : c0 Star4 0xefd0025aa29 @ 43 : 0c LdaZero 0xefd0025aa2a @ 44 : 1c f6 TestReferenceEqual r4 0xefd0025aa2c @ 46 : 98 05 JumpIfTrue [5] (0xefd0025aa31 @ 51)
以上bytecode非常清晰展示了async function整个执行过程。
再以直观的示意图来加深印象:
注意: V8 v7.2之前,无论v值类型都会为其创建临时promise实例
由图中分析:
-
创建
implicit_promise
作为async function返回值,同时createPromise
创建临时promise实例用于对v值(命名为promise_42)进行resolvePromise
-
针对每一个await point,首先通过
performPromiseThen
将async function的resume/throw
注册到promise_42。然后suspend挂起,等待promise_42处理。一旦fulfilled执行resume并将数据返回await point,进入下一个await point;而rejected就执行throw退出async function。 -
最后async function并将
implicit_promise
返回给调用者,同时归还执行权,调用者继续执行后续script代码,如对implicit_promise
进行then
方法调用等。 -
foo函数等待
ResolvePromise(promise, promise_42)
,此时由于ResolvePromise入参promise_42是一个promise实例,会创建PromiseResolveThenableJob
放入microtask队列等待,而
PromiseResolveThenableJob
又会创建PromiseReactionJob
V8 v7.2之后,根据v值类型判断是否创建临时Promise实例
图左红底代码块逻辑事实上等价于PromiseResolve
如图右绿底代码,利用PromiseResolve
built-in实现,当v值是promise实例时直接复用,即不再创建临时promise实例,这样避免ResolvePromise
入参为promise实例而额外创建PromiseResolveThenableJob
,而是直接创建PromiseReactionJob
。
在最后一个await point接收到从async function resume的返回值42时,由所有await point构建的promise chain进入最后一跳,也就是implicit_promise
。async function成功退出,对应implicit_promise
进入fulfilled;而如果因任一await point抛错导致的失败退出,对应implicit_promise
就进入了rejected,调用者通过then
注册的对应状态handler也会执行。
await如何执行
上面分析了await关键字的实现原理,接下来要分析一下await关键字如何执行,也就是cxx层面的built-in方法AsyncFunctionBuiltinsAssembler::AsyncFunctionAwait
首先着重看看注释部分,表达的信息很重要:
AsyncFunctionAwait
是await关键字的核心逻辑,V8将await value
语法糖反解成yield AsyncFunctionAwait(.generator_object, value)
,表明async function在cxx层面确实是以Generator
为基础而封装实现的
OK,接着看一下AsyncFunctionAwait
的具体处理逻辑:
-
首先
Return(outer_promise)
表明AsyncFunctionAwait
返回promise实例,就像foo函数例子里的implicit_promise
一样。本质上,从下面这段代码可以发现outer_promise
是贯穿于async function的整个执行过程的,简而言之,每一个await point都与这个outer_promise
关联。TNode<JSPromise> outer_promise = LoadObjectField<JSPromise>( async_function_object, JSAsyncFunctionObject::kPromiseOffset)
-
然后重点要提到
AsyncFunctionAwaitResolveSharedFunConstant
和AsyncFunctionAwaitRejectSharedFunConstant
,由它们生成Promise层面的resolve/reject方法对
在所有准备工作完成之后,await就进入真正的处理逻辑AsyncBuiltinsAssembler::Await
首先针对await接收的value值进行PromiseResolve处理,对于PromiseResolve的逻辑分析可以参考《这就是编程:你知道在cxx语言层面Promise如何实现吗》,最后得到一个wrapper promise实例。
- 接着初始化await的上下文,这里的上下文不是async function对应的
JSAsyncFunction
实例async_function_object
,可以理解是接下来马上要创建的onFulfilled/onRejected handlers的闭包上下文 - 同时因为在闭包上下文中无法获取到
async_function_object
,所以要将它存入闭包上下文的扩展字段,这样onFulfilled/onRejected handlers执行时就能获取到正确的async_function_object
,也就能对其进行resume/throw操作
接下来就是创建onFulfilled/onRejected handlers,在async function里,它们包含了resume/throw执行逻辑,通过它们就能从suspend point恢复或者抛错退出。
resume/throw对应bytecode中ResumeGenerator/Rethrow
其实整个await执行过程,除了在BuildAwait
时BuildSuspendPoint
引入了suspend point,其他就是纯粹的Promise处理逻辑。接下来就是对wrapper promise实例调用一次PerformPromiseThenImpl
,如下代码所示,而此时也会传递上面创建的onFulfilled/onRejected handlers。最后一旦onFulfilled,async function就从await point恢复继续执行,然后继续后面的await point的执行;而一旦onRejected,async function就会在suspend point抛错退出
performPromiseThen(
promise,
(res) => resume(<<foo>>, res),
(err) => throw(<<foo>>, err),
throwaway
)
<<foo>>
就是async_function_object,作为resume/throw的执行上下文
注意:
事实上,resume/throw在cxx层面是由generator
的next/throw来实现,且async function里的return对应了Generator.prototype.return
,本质上也是一次await point处理,只不过在return时,generator
实例的next方法返回值中done属性为true,表示generator
迭代完结,从而停止触发next方法。
小结
OK,目前已经分析了async function在V8中整个执行过程,总结起来它的实现原理是:
- 以Generator协程机制为基础,由Promise实现异步执行
- 通过promise chain搭配Generator的resume/rethrow达到await“等待”执行的同步效果
相信大家对async/await也有了新的认识,接下来继续来看一些和JavaScript async function有关的优秀源码,比如regenerator-runtime。
regenerator-runtime
在JavaScript业界,提到async/await,就不得不提regenerator-runtime,它是facebook/regenerator中关于async function的runtime实现。
鉴于当前兼容性问题,async function有时不能以原生的方式使用,因此会使用regenerator-runtime来模拟async function的runtime。
facebook/regenerator是一个monorepo,不仅包括regenerator-runtime,也提供了regenerator命令,通过regenerator --include-runtime
可以生成自带regenerator-runtime的async function转译代码,如转译后的foo函数,这里省略了regenerator-runtime代码:
function foo() {
var v;
return regeneratorRuntime.async(function foo$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return regeneratorRuntime.awrap(42);
case 2:
v = _context.sent;
return _context.abrupt("return", v);
case 4:
case "end":
return _context.stop();
}
}, null, null, null, Promise);
}
发现async function被转译成regeneratorRuntime.async
,同时async function的函数体变成了while(1)无限循环。
往往regenerator转译后的代码给人的印象,甚至说误解:
- 调用栈紊乱,难以调试,尤其是while(1)无限循环的出现
- 增加代码大小影响性能
但希望在明白它的实现原理之后能缓解以上这些误解
Generator runtime
之所把它放在async function在V8的实现原理分析之后,是因为regenerator-runtime完美复刻了V8的async function实现原理:
- 以Generator协程机制为基础,由Promise实现异步执行
- 通过promise chain搭配Generator的resume/rethrow达到await“等待”执行的同步效果
- 首先获取
AsyncIterator
实例iter - 通过promise chain配合
iter.next来
实现await“等待”效果,因为当AsyncIterator
迭代器未完结时就会由promise onFulfilled触发下一次iter.next
继续迭代下去 - 最后将promise chain返回给调用者
wrap
函数的目的就如其字面意思一样,包装generator
实例,同时定义一个_invoke
作为Generator
原生方法next
、return
、throw
的泛化调用。
在AsyncIterator
构造器里:
- 将包装的
generator
实例封装进AsyncIterator
实例作为协程机制的基础触发点。 - 定义一个
_invoke
属性,以enqueue
函数作为属性值,enqueue
的作用是始终返回一个promise实例,并同时触发generator
持续迭代,以promise chain来响应generator
的迭代值或抛错。
Suspend point
经过以上分析明白了regenerator-runtime内部如何通过Generator
配合Promise
来模拟await,但是对于调用者,是怎么在所谓的await point位置获取正确的结果值呢?
这一切都归功于Context
实例,它巧妙的模拟了suspend point
var context = new Context(tryLocsList || []);
在V8执行async function时,对每一个await point都会构建一个suspend point并设置对应的suspend_id
;而在regenerator-runtime中,所有的suspend point都在while(1)
中的switch
里,每一个case就是一个suspend point,准确的说,这些suspend point是在regenerator的ast解析过程中生成的,而所有的suspend_id
对应await point的ast node的index索引值。
对于await point,当generator
实例正确执行next/return时,suspend point处恢复的执行权按顺序从上往下通过context.next
来传递,而当generator
实例throw抛错时,则在context
里throw context.arg
退出并进入promise chain的onRejected handler。
现在理解了regenerator-runtime的实现原理,后续调试肯定会得心应手了。
结尾
经过上一期《这就是编程:你知道在cxx语言层面Promise如何实现吗》和本期对async function在cxx层面的实现原理分析,相信对JavaScript异步编程肯定有了更深的理解:
- 了解了await语法糖如何实现
- 了解了async function在V8内部以Generator协程机制为基础
- 了解了async function如何通过promise chain巧妙实现await“等待”效果
OK,本期先分享到这里,如果读者有任何疑惑,欢迎评论区留言讨论👏🏻,笔者很乐意与大家分享源码阅读的心得,也是一次很好的自我学习机会🤔。
Bye bye,下期同一时间再见👋🏻。
参考资料
转载自:https://juejin.cn/post/7203993298818547772