事情的起因是这样的,一直很好奇,async/await语句在runtime中是怎么执行的,于是便查阅了Ecmascript标准文档,下面我们由此展开。

async和await的执行过程

AsyncFunctionStart

如图,在25.7.5.1 AsyncFunctionStart中,running execution context被复制了一份命名为asyncContext(step1-2),并推入了execution context stack中(step4),所以asyncContext为当前的执行环境,当asyncContext被执行时,首先计算asyncFunctionBody,然后asyncContext会被移除出执行环境栈,并把当前栈顶的执行环境作为running execution context(step3),此时恢复被挂起的asyncContext,并将返回值赋值给result(step5)。

下面我们看下await的执行过程

Await

promise相关的调用链

如上图,在6.2.3.1 Await中,在step10中,会执行PerformPromiseThen方法,该方法的执行过程如下

PerformPromiseThen

由上图中的PerformPromiseThen依次找下去,得到了下面一张调用关系图

promiseInternalPrinciple

其中,各个节点的原文地址如下: Promise constructor PromiseResolveThenableJob CreateResolvingFunction Promise Resolve Functions Promise Reject Functions FulfillPromise RejectPromise TriggerPromiseReactions PerformPromiseThen

在上图的最后一步,都会执行EnqueueJob方法,该方法对应的执行逻辑如下

EnqueueJob

EnqueueJob

如上图,在8.4.1中,该方法会把对应的PromiseReactionJob或PromiseResolveThenableJob放入Job Queue中(step9)

Job Queue

说到```Job Queue``,标准中有如下描述:

JobQueue

JobQueue2

如上,在8.4中,Job被定义为一个抽象操作,该操作用来初始化一个ECMAScript计算。

一个Pending Job是一个对将来要执行的Job的请求。

一个Job Queue是一个PendingJob record的先进先出队列。每个Job Queue都有一个名字和一个可用的Job Queue集合;并且被一种ECMAScript实现所定义。每个ECMAScript实现都至少有两种Job Queues,一种是ScriptJobs,另一种是PromiseJobs

ScriptJobs用于校验和计算ECMAScript脚本和Module源文本。

PromiseJobs用于响应Promise语句。

关于Job Queues的执行时间,上面也有描述: 当没有正在运行的执行环境,并且执行环境栈为空时,ECMAScript实现移除Job Queue中的第一个PendingJob,以此创建一个执行环境,并开始执行相关联的Job抽象操作。

RunJobs

关于这些Job是怎么执行的,标准中有如下描述:

RunJobs Suspend

如上,在8.624.4.1.9中,描述了ScriptJobs的执行过程。

  1. 首先执行宿主定义的区域初始化
  2. 获取ECMAScript源文本,如果该源文本是一个script的源代码,则执行一个入队操作,将ScriptEvaluationJob推入Job Queue;如果该源文本是一个module的源代码,则将TopLevelModuleEvaluationJob推入Job Queue
  3. 重复:
    1. 挂起running execution context,并从执行环境栈移除他
    2. 断言:执行环境栈现在是空的
    3. 设置nextQueue为一个非空的Job Queue,其选择的方式由宿主实现来决定。如果所有的Job Queue都是空,则结果由宿主实现来决定。
    4. 设置nextPending为nextQueue前端的PendingJob record,并从nextQueue中移除PendingJob record。 5-8. 初始化newContext
    5. 将newContext推入执行环境栈;newContext现在为running execution context
    6. 使用nextPending来执行任何实现或宿主环境定义的job初始化。
    7. 设置result为nextPending.[[Job]]的执行结果。

24.4.1.9中,描述了Suspend的执行过程。

Suspend(WL, W, timeout) WL: a WaiterList W: an agent signifier timeout: 非负、非NaN数字

  1. 断言:正在调用的agent在WL的临界区
  2. 断言:W与AgentSignifier相等
  3. W在WL的等待列表中
  4. AgentCanSuspend()为true
  5. 执行LeaveCriticalSection(WL),并且挂起W,直到timeout毫秒为止;当一个通知在临界区退出后但挂起生效之前到达时,执行被合并的操作。W可以因为timeout到期被通知,或者因为被另一个agent调用NotifyWaiter显式通知,不会因为其他原因。
  6. 执行EnterCriticalSection(WL)
  7. 如果W被显式通知,返回true
  8. 返回false。

Execution Contexts

一个execution context是一个标准设备,被ECMAScript实现用来记录运行时的代码执行。在任何一个时间点,每个agent有最多一个执行环境,即running execution context

execution context stack被用于记录执行环境。running execution context总是这个栈的顶部元素。当控制从与当前执行环境相关联的可执行代码转移到与当前执行环境无关的可执行代码时,一个新的执行环境会被创建。

ExecutionContexts

如上图,在8.3中,列出了所有执行环境的状态组件:

  1. code evaluation state:
  2. Function
  3. Realm
  4. ScriptOrModule

对于ECMAScript code的执行环境,还有额外的两个状态组件:

  1. LexicalEnvironment
  2. VariableEnvironment

对于Generator执行环境,还有额外的一个状态组件:

  1. Generator

上面的状态对象中有提到Realm, 下面我们详细说下什么是Realm

Realm

在代码执行之前,所有的ECMAScript code必须关联一个realm。从概念上说,一个realm包括一个内置对象的集合,一个ECMAScript全局环境,被导入到全局环境的所有ECMAScript code,以及其它被关联的状态和资源。

一个realm在该标准中表示一个Realm Record,一个Realm Record有如下指定字段:

  1. [[Intrinsics]]: 一个内置对象的集合
  2. [[GlobalObject]]: 全局对象
  3. [[GlobalEnv]]: 全局环境
  4. [[TemplateMap]]: 模板对象
  5. [[HostDefined]]: 宿主环境保留的字段,需要关联Realm Record的其他信息。