Async Function 背后的秘密

因为能力有限,不免会有疏漏不妥之处,还请不吝赐教!也欢迎你们积极讨论

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

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

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

调试代码

通过调试,发现问题的关键是如下代码github

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后面.我作了张调试的对比图,能够比较直观的感觉到差异:segmentfault

compare.png

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

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

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

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

接下来,尝试从语言规范的角度去解释一下如下代码,但愿能跟你们一块儿从另一个角度去理解这段代码在实际运行中到底作了什么.异步

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

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

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

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

async-function.png

步骤解析

EvaluateAsyncFunctionBody

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

Runtime Semantics: EvaluateAsyncFunctionBody

关键步骤:

  • 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的定义

AsyncFunctionStart

关键步骤:

  • 1. 设置runningContextrunning 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 都是怎么执行的.

Promise Resolve Functions

关键步骤,主要针对执行 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)
    })
  })
}

这里再放一张对比图,你们能够找找看跟前面一张有什么不一样

compare2.png

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

鉴于我也没有多少面试的经验,前不久刚搞砸了一场面试 😭,下面纯属我我的的 yy,没有多少实践基础,你们能够把这看成一个思路做为参考,若是有不对的地方欢迎补充和讨论

当咱们遇到这种题,若是以前有研究过,那能给出正确的答案当然好.不过有可能会遇到一些题,平常使用中,基本上不会遇到,因此基本对这类边界状况可能不会有接触,好比像这道题,不过不知道也有不知道的解法,面试官出题的目的是为了考察面试者的知识,以掌握面试者的能力.

像这种状况能够直接把本身求解的过程描述给面试官,这样能经过这个过程把本身掌握的相关知识呈现给面试官,这也是面试官所想要的.还能够请教面试官正确的解法或者若是找到相关资料,从中展示本身的求知欲.也能够描述本身日常是如何去编写异步代码的,若是是顺序相关的异步会明确前后顺序的使用 then 串联,或者使用 await 关键词,保证顺序是肯定的,而若是是顺序不相关的异步,遇到这种状况也没太大关系.这能够展示本身良好的编程能力.

另一个怪异现象

在调试的过程当中发现另一个使人费解的状况,若是在Promise.resolve()以前加一个await,竟然能让after:foo提早,排在tick:3后面,这又是一个使人费解的现象.

其实这是由于规范以前针对await作过一次优化,若是await后面跟着的值是一个 Promise 的话,这个优化会少建立两次微任务,更多详情能够查看下面的文章:

Node.js v10中尚未这个优化,因此咱们能够实际验证一下:

Comparison of different Node.js

ES 规范阅读

  • 基础(这些基础属于非必须条件)

    • 文法,语法,词法之类的基础知识
    • BNF 产生式
    • 有必定的 JavaScript 基础

前两个基础,若是有了解的话是最好的,没有也影响不大,至于第三个基础,若是没有的话,难度会有点大 😂

推荐资料

在下面的资源中,把推荐阅读列表读完,基本上就能自行去阅读规范了.不过刚开始可能会有一些难度,好比遇到某个语句不知道什么意思,或者为何这里会出现这种语句之类的疑问,这时候能够经过搜索引擎去搜索相关关键字找到答案.另外这些文章是能够反复阅读,也许每次阅读都会有不同的收获.

官方的规范有两个地方能够看到,https://tc39.eshttps://www.ecma-internationa... 均可以,不过官方的规范都是放在一个页面上的,每次打开都须要加载全部内容,速度会很是慢.

这里推荐一个项目read262.用read262的话,能够分章节阅读,要查看某个章节,只须要加载那个章节的内容,当须要打开规范多个部分进行对照时会很方便.不过read262会根据 https://tc39.es/ecma262 的更新自动重键,因此只有最新的规范内容,若是须要看其余版本的规范,仍是须要到ECMA-262去看对应的版本.read262能够直接使用在线版 https://read262.jedfox.com

JS 引擎

推荐一个库engine262,以前我说看引擎的源码会更接近实现,只是碍于阅读难度来讲,阅读规范会更容易一些.其实有一个库是用 JavaScript 实现的引擎,这样源码的阅读难度显然小了不少.不过我推荐仍是先去看规范,而后在实际去engine262源码中查看对应的实现,最后还能够将代码下载到本地运行,实际去调试源码的运行的过程,以印证对规范的理解.

engine262会根据最新的规范去实现,而咱们看的有时候不必定是最新的规范, engine262也没有依据规范的版本去作标记.这里有一个小技巧,能够先找到实现规范对应的源码,而后看那个文件的提交记录,找到跟规范修改的时间相吻合的提交,而后去看那个提交中的实现就跟规范中的描述一致了.

写在最后

这篇文章经过一个例子,展现如何经过阅读规范去找到隐藏在 JavaScript 代码背后的秘密.固然若是仅仅只是得出一个结论,其实并无多大意义,像例子中的状况,属于很是边界的状况,现实中能遇到的几率应该不大.

我但愿能经过这篇文章让你们更多的了解规范,而且经过上面列出的资料去学习和掌握如何阅读规范的技巧,这样当咱们遇到某些问题时能够去找到最权威的资料解答疑惑.不过规范中的大多数知识可能对于咱们平常开发都太大帮助,咱们只须要掌握阅读的技巧,在有须要的时候去翻翻它便可.