likes
comments
collection
share

这就是编程:async/await原来就这?

作者站长头像
站长
· 阅读数 32

前言

“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”

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

执行过程

这就是编程:async/await原来就这?

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为原型。

这就是编程:async/await原来就这?

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/await原来就这?

  • 首先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处理

这就是编程:async/await原来就这?

VisitAwait里,对当前await point保存现场,然后对其进行BuildAwait,使得每一个await point成为resumable,即可挂起和可恢复,具体看看BuildAwait是如何处理:

这就是编程:async/await原来就这?

  1. 首先为register寄存器设置正确的作用域,因为这关系到await point在LoadAccumulatorWithRegister时能否从正确的register里获取结果值,即在JavaScript层面await能否获取到预期结果值

  2. 确定await关键字的FunctionId,这个FunctionId直接决定了await在cxx层面的逻辑处理,即怎么处理跟随的“数据”。上面bytecode里调用的InvokeIntrinsic [_AsyncFunctionAwaitUncaught]正是FunctionId所指向的cxx内部AsyncFunctionBuiltinsAssembler的built-in方法AsyncFunctionAwaitUncaught

     0xefd0025aa16 @   24 : 68 01 f7 02       InvokeIntrinsic [_AsyncFunctionAwaitUncaught], r3-r4
    

    这就是编程:async/await原来就这?

  3. 绑定FunctionId作为await的逻辑处理函数,然后获取register列表,并作为闭包参数传给await使用

  4. 接着BuildSuspendPoint这就是编程:async/await原来就这?为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
    
  5. 最后判断当前async_function_object是否需要resumeresume_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整个执行过程。

再以直观的示意图来加深印象:

这就是编程:async/await原来就这?

注意: V8 v7.2之前,无论v值类型都会为其创建临时promise实例

由图中分析:

  1. 创建implicit_promise作为async function返回值,同时createPromise创建临时promise实例用于对v值(命名为promise_42)进行resolvePromise

  2. 针对每一个await point,首先通过performPromiseThen将async function的resume/throw注册到promise_42。然后suspend挂起,等待promise_42处理。一旦fulfilled执行resume并将数据返回await point,进入下一个await point;而rejected就执行throw退出async function。

  3. 最后async function并将implicit_promise返回给调用者,同时归还执行权,调用者继续执行后续script代码,如对implicit_promise进行then方法调用等。

  4. foo函数等待ResolvePromise(promise, promise_42),此时由于ResolvePromise入参promise_42是一个promise实例,会创建

    PromiseResolveThenableJob

    放入microtask队列等待,而PromiseResolveThenableJob又会创建PromiseReactionJob

V8 v7.2之后,根据v值类型判断是否创建临时Promise实例

图左红底代码块逻辑事实上等价于PromiseResolve

这就是编程:async/await原来就这?

如图右绿底代码,利用PromiseResolvebuilt-in实现,当v值是promise实例时直接复用,即不再创建临时promise实例,这样避免ResolvePromise入参为promise实例而额外创建PromiseResolveThenableJob,而是直接创建PromiseReactionJob

这就是编程:async/await原来就这?

在最后一个await point接收到从async function resume的返回值42时,由所有await point构建的promise chain进入最后一跳,也就是implicit_promise。async function成功退出,对应implicit_promise进入fulfilled;而如果因任一await point抛错导致的失败退出,对应implicit_promise就进入了rejected,调用者通过then注册的对应状态handler也会执行。

这就是编程:async/await原来就这?

await如何执行

上面分析了await关键字的实现原理,接下来要分析一下await关键字如何执行,也就是cxx层面的built-in方法AsyncFunctionBuiltinsAssembler::AsyncFunctionAwait

这就是编程:async/await原来就这?

首先着重看看注释部分,表达的信息很重要:

  • 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)
    
  • 然后重点要提到AsyncFunctionAwaitResolveSharedFunConstantAsyncFunctionAwaitRejectSharedFunConstant,由它们生成Promise层面的resolve/reject方法对

在所有准备工作完成之后,await就进入真正的处理逻辑AsyncBuiltinsAssembler::Await

这就是编程:async/await原来就这?

首先针对await接收的value值进行PromiseResolve处理,对于PromiseResolve的逻辑分析可以参考《这就是编程:你知道在cxx语言层面Promise如何实现吗》,最后得到一个wrapper promise实例。

这就是编程:async/await原来就这?

  • 接着初始化await的上下文,这里的上下文不是async function对应的JSAsyncFunction实例async_function_object,可以理解是接下来马上要创建的onFulfilled/onRejected handlers的闭包上下文
  • 同时因为在闭包上下文中无法获取到async_function_object,所以要将它存入闭包上下文的扩展字段,这样onFulfilled/onRejected handlers执行时就能获取到正确的async_function_object,也就能对其进行resume/throw操作

这就是编程:async/await原来就这?

接下来就是创建onFulfilled/onRejected handlers,在async function里,它们包含了resume/throw执行逻辑,通过它们就能从suspend point恢复或者抛错退出。

resume/throw对应bytecode中ResumeGenerator/Rethrow

这就是编程:async/await原来就这?

其实整个await执行过程,除了在BuildAwaitBuildSuspendPoint引入了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中整个执行过程,总结起来它的实现原理是:

  1. 以Generator协程机制为基础,由Promise实现异步执行
  2. 通过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实现原理:

  1. 以Generator协程机制为基础,由Promise实现异步执行
  2. 通过promise chain搭配Generator的resume/rethrow达到await“等待”执行的同步效果

这就是编程:async/await原来就这?

  1. 首先获取AsyncIterator实例iter
  2. 通过promise chain配合iter.next来实现await“等待”效果,因为当AsyncIterator迭代器未完结时就会由promise onFulfilled触发下一次iter.next继续迭代下去
  3. 最后将promise chain返回给调用者

这就是编程:async/await原来就这?

wrap函数的目的就如其字面意思一样,包装generator实例,同时定义一个_invoke作为Generator原生方法nextreturnthrow的泛化调用。

这就是编程:async/await原来就这?

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抛错时,则在contextthrow 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
评论
请登录