Async Function 背后的秘密

由于能力有限,难免会有疏漏不妥之处,还请不吝赐教!也欢迎大家积极讨论

前几天看到一道题async 输出顺序的一道前端面试题疑问

async function async1() {
  console.log(\'async1 start\')
  await async2()
  console.log(\'async1 end\')
}

async function async2() {
  console.log(\'async2 start\')
  return new Promise((resolve, reject) => {
    resolve()
    console.log(\'async2 promise\')
  })
}

async1()

new Promise(function (resolve) {
  console.log(\'promise1\')
  resolve()
})
  .then(function () {
    console.log(\'promise2\')
  })
  .then(function () {
    console.log(\'promise3\')
  })

自己算了下,得到跟题主一样的疑惑,为什么async1 end会跑到promise3的后面,怎么算都应该在promise2后面

我的理解 实际输出
async1 start async1 start
async2 start async2 start
async2 promise async2 promise
promise1 promise1
promise2 promise2
async1 end promise3
promise3 async1 end

既然理解跟实际结果的有出入,那肯定是哪里理解不到位,先调试看看到底是哪一段代码出了问题

调试代码

经过调试,发现问题的关键是以下代码

async function async2() {
  console.log(\'async2 start\')
  return new Promise((resolve, reject) => {
    resolve()
    console.log(\'async2 promise\')
  })
}

为了演示方便,做了一些修改:

new Promise(function (resolve) {
  console.log(\'tick: 1\')
  resolve()
})
  .then(() => console.log(\'tick:2\'))
  .then(() => console.log(\'tick:3\'))
  .then(() => console.log(\'tick:4\'))
  .then(() => console.log(\'tick:5\'))

async function foo() {
  return Promise.resolve()
}
foo().then(() => {
  console.log(\'after:foo\')
})

输出顺序如下:

tick:1
tick:2
tick:3
tick:4
after:foo
tick:5

经过反复调试发现,如果 foo 不加 async 关键字,或者不返回 Promise,结果都符合预期,after:foo出现在tick:2后面.而如果这两个同时出现的时候,按照我的理解after:foo应该出现在tick:3后面,但是实际结果却比预期额外多一个tick,出现在tick:4后面.我做了张调试的对比图,可以比较直观的感受到差别:

Async Function 背后的秘密

这里说明我的理解不到位,那就需要去研究清楚这段代码到底发生了什么.

正好之前看过一些词法语法以及产生式之类的知识,于是想尝试从 ES 规范中找找,看能不能找到答案,就当作练习如何阅读规范了。

结果证明我还是太年轻了,刚开始就看的我头皮发麻,根本看不懂,本来英语对我来说就已经是天书了,加上规范中各种独创的语法,真的是要了亲命了,不过好在有各路大神和前辈的文章(后面会列出相关的这些文章),讲解怎么去阅读规范,通过慢慢学习,总算是把涉及到的相关方面都理清楚了.

从 ECMAScript 规范角度去解释代码的运行

接下来,尝试从语言规范的角度去解释一下以下代码,希望能跟大家一起从另外一个角度去理解这段代码在实际运行中到底做了什么.

重新放一下代码,我的理解 async 关键字会产生一个 Promise,加上返回的 Promise 最多两个微任务,而实际运行中却是多了个微任务,要搞清楚多出的一个是从哪里来的.

async function foo() {
  return Promise.resolve()
}

先用一张图理一下整体的流程

限于我这还没入门的英语水平,就不一一翻译了,有需要的朋友可以点击链接直接看原文,如果跟我一样英语比较差的,可以用百度翻译谷歌翻译之类的工具。红框中是涉及到相关章节,后续只解释其中的关键步骤.

Async Function 背后的秘密

步骤解析

EvaluateAsyncFunctionBody

我们首先找到15.8.4 Runtime Semantics: EvaluateAsyncFunctionBody,这里定义了AsyncFunction是如何执行的

Async Function 背后的秘密

关键步骤:

  • 1. 执行抽象操作NewPromiseCapability,该操作会返回一个PromiseCapability Record { [[Promise]]: promise, [[Resolve]]: resolve, [[Reject]]: reject },将其赋值给promiseCapability
  • 2. 抽象操作FunctionDeclarationInstantiation执行函数声明初始化,像参数变量的声明,各种情况的说明,跟本文没有很大关系
  • 3. 如果实例化没有错误,则执行AsyncFunctionStart(promiseCapability, FunctionBody)
  • ...

AsyncFunctionStart

接下来我们进到 27.7.5.1 AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) 看看AsyncFunctionStart的定义

Async Function 背后的秘密

关键步骤:

  • 1. 设置runningContext为running execution context
  • 2. 设置asyncContextrunningContext的副本
  • 4. 设置asyncContext恢复后需要执行的步骤

    • a. 设置resultasyncFunctionBody的执行结果
    • ...
    • e. 如果result.[[Type]]return,则执行Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »)
  • ...

这里关键的是第 4 步中的执行步骤,对于我们要理解的 foo 函数来说,会先执行Promise.resolve(),得到结果Promise {<fulfilled>: undefined},然后返回,所以result.[[Type]]return,会执行 4.e 这一步.

最终到 4.e 执行Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »), Call是一个抽象操作,这句最后相当于转换成promiseCapability.[[Resolve]](« result.[[Value]] »).promiseCapability是一个PromiseCapability Record规范类型,在 27.2.1.1 PromiseCapability Records 中能看到PromiseCapability Record的定义

Promise Resolve Functions

顺着往下找,能找到27.2.1.3.2 Promise Resolve Functions的定义,接下来看看 resolve 都是怎么执行的.

Async Function 背后的秘密

关键步骤,主要针对执行 resolve 时传入参数的不同,而执行不同的操作

  • resolve 方法接收参数resolution
  • 7. 使用SameValue(resolution, promise)比较resolutionpromise,如果为 true,则返回RejectPromise(promise, selfResolutionError),我的理解是为了避免自身循环引用,例:

    let f
    const p = new Promise(resolve => (f = resolve))
    f(p)
  • 8 - 12. 如果resolution不是对象,或者resolution是一个对象但resolution.then不是方法,则返回FulfillPromise(promise, resolution),例:

    // 8, resolution 不是对象
    new Promise(r => r(1))
    // 12, resolution.then 不是方法
    new Promise(r => r({ a: 1 }))
    new Promise(r => r({ then: { a: 1 } }))
  • 13. 设置thenJobCallbackHostMakeJobCallback(resolution.then.[[Value]])执行的结果JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }
  • 14. 设置 job 为NewPromiseResolveThenableJob(promise, resolution, thenJobCallback)执行的结果Record { [[Job]]: job, [[Realm]]: thenRealm }

    • 上面这两步就是关键所在,这里的 job 会额外创建一个微任务,类似下面的伪代码:

      function job() {
        const resolution = { then() {} }
        const thenJobCallback = {
          [[Callback]]: resolution.then,
          [[HostDefined]]: empty,
        }
        return new Promise(function (resolve, reject) {
          thenJobCallback[[Callback]](resolve)
        })
      }
  • 15. 执行HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]])

    • 这一步也会创建一个微任务,加上 job,如果传入的 resolution 还是一个 Promise 的话,那 resolution.then 还会创建一个微任务,这就解释了,为什么当在 Async Function 中返回 Promise 之后,after:foo会在tick:4之后出来

结论

至此我们可以知道中间的三个微任务都是哪里来的了:

  • HostEnqueuePromiseJob会创建一个微任务,这个微任务执行时,会去执行 NewPromiseResolveThenableJob返回的 job
  • NewPromiseResolveThenableJob返回的 job 执行时会创建一个微任务,当这个微任务执行时,去执行resolution.then
  • 加上如果resolution是一个 Promise,那执行 then 时,还会创建一个微任务

这其中NewPromiseResolveThenableJob返回的 job 就是之前我不知道的那点.这些都是 js 引擎在后面处理的,我们平常是没有感知的.如果不通过阅读规范,估计很难搞清楚这背后都发生了什么.

其实还有一种方法可以更接近实际运行的过程,就是去查看规范实现(既 js 引擎,比如 V8)的源码,不过相对来说可能阅读规范会比 C++ 的源码来的更容易一些.

为了方便记忆和理解,可以用 Promise 做如下转换

暂时执行结果是等价的,不过有可能之后会随着标准的修改,或者 JS 引擎实现的不同而有差异.
async function foo() {
  return Promise.resolve()
}
// =>
function foo() {
  const p = Promise.resolve()
  return new Promise(function (resolve, reject) {
    resolve(p)
  })
}
// =>
function foo() {
  const p = Promise.resolve()
  return new Promise(function (resolve, reject) {
    Promise.resolve().then(() => {
      p.then(resolve)
    })
  })
}

这里再放一张对比图,大家可以找找看跟前面一张有什么不同

Async Function 背后的秘密

关于面试时遇到这道题的\"解法\"

鉴于我也没有多少面试的经验,前不久刚搞砸了一场面试
上一篇:比HashMap更快的查询


下一篇:GLEAN: Generative Latent Bank for Large-Factor Image Super-Resolution