关于javascript:Async-Function-背后的秘密

66次阅读

共计 7285 个字符,预计需要花费 19 分钟才能阅读完成。

因为能力无限, 难免会有疏漏不妥之处, 还请不吝赐教!也欢送大家踊跃探讨

前几天看到一道题 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 前面. 我做了张调试的比照图, 能够比拟直观的感触到差异:

这里阐明我的了解不到位, 那就须要去钻研分明这段代码到底产生了什么.

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

后果证实我还是太年老了, 刚开始就看的我头皮发麻, 基本看不懂, 原本英语对我来说就曾经是天书了, 加上标准中各种独创的语法, 真的是要了亲命了, 不过好在有各路大神和前辈的文章(前面会列出相干的这些文章), 解说怎么去浏览标准, 通过缓缓学习, 总算是把波及到的相干方面都理分明了.

从 ECMAScript 标准角度去解释代码的运行

接下来, 尝试从语言标准的角度去解释一下以下代码, 心愿能跟大家一起从另外一个角度去了解这段代码在理论运行中到底做了什么.

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

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

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

限于我这还没入门的英语水平, 就不一一翻译了, 有须要的敌人能够点击链接间接看原文, 如果跟我一样英语比拟差的, 能够用百度翻译谷歌翻译之类的工具。红框中是波及到相干章节, 后续只解释其中的关键步骤.

步骤解析

EvaluateAsyncFunctionBody

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

关键步骤:

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

关键步骤:

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

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

这里再放一张比照图, 大家能够找找看跟后面一张有什么不同

对于面试时遇到这道题的 ” 解法 ”

鉴于我也没有多少面试的教训, 前不久刚搞砸了一场面试 😭, 上面纯属我集体的 yy, 没有多少实际根底, 大家能够把这当作一个思路作为参考, 如果有不对的中央欢送补充和探讨

当咱们遇到这种题, 如果之前有钻研过, 那能给出正确的答案诚然好. 不过有可能会遇到一些题, 日常应用中, 基本上不会遇到, 所以根本对这类边界状况可能不会有接触, 比方像这道题, 不过不晓得也有不晓得的解法, 面试官出题的目标是为了考查面试者的常识, 以把握面试者的能力.

像这种状况能够间接把本人求解的过程形容给面试官, 这样能通过这个过程把本人把握的相干常识出现给面试官, 这也是面试官所想要的. 还能够求教面试官正确的解法或者如果找到相干材料, 从中展示本人的求知欲. 也能够形容本人平时是如何去编写异步代码的, 如果是程序相干的异步会明确先后顺序的应用 then 串联, 或者应用 await 关键词, 保障程序是确定的, 而如果是程序不相干的异步, 遇到这种状况也没太大关系. 这能够展示本人良好的编程能力.

另外一个怪异景象

在调试的过程中发现另外一个令人费解的状况, 如果在 Promise.resolve() 之前加一个 await, 竟然能让after:foo 提前, 排在 tick:3 前面, 这又是一个令人费解的景象.

其实这是因为标准之前针对 await 做过一次优化, 如果 await 前面跟着的值是一个 Promise 的话, 这个优化会少创立两次微工作, 更多详情能够查看上面的文章:

  • Faster async functions and promises

    • 更快的异步函数和 Promise
    • v8 是怎么实现更快的 await?深刻了解 await 的运行机制

Node.js v10 中还没有这个优化, 所以咱们能够理论验证一下:

ES 标准浏览

  • 根底(这些根底属于非必须条件)

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

前两个根底, 如果有理解的话是最好的, 没有也影响不大, 至于第三个根底, 如果没有的话, 难度会有点大 😂

举荐材料

在上面的资源中, 把举荐浏览列表读完, 基本上就能自行去浏览标准了. 不过刚开始可能会有一些难度, 比方遇到某个语句不晓得什么意思, 或者为什么这里会呈现这种语句之类的疑难, 这时候能够通过搜索引擎去搜寻相干关键字找到答案. 另外这些文章是能够重复浏览, 兴许每次浏览都会有不一样的播种.

  • 举荐浏览

    • Understanding the ECMAScript spec 系列

      • [[译]了解 ECMAScript 标准(1)](https://lisongfeng.cn/2020/09…
      • [[译]了解 ECMAScript 标准(2)](https://lisongfeng.cn/2020/09…
      • [[译]了解 ECMAScript 标准(3)](https://lisongfeng.cn/2020/09…
      • [[译]了解 ECMAScript 标准(4)](https://lisongfeng.cn/2020/09…
    • ECMAScript 浏览指南

      • ECMAScript 浏览指南(一)具体介绍了标准中的一些根底概念
      • ECMAScript 浏览指南(二)
    • How to Read the ECMAScript Specification

      • 中文版 怎么浏览 ECMAScript 标准?
    • ECMAScript 标准外围术语(继续更新)
  • 其余

    • ECMAScript 标准
    • ECMAScript-ReadNotes

      • https://juejin.cn/post/684490…
    • https://www.youtube.com/c/ben…

官网的标准有两个中央能够看到,https://tc39.es 和 https://www.ecma-internationa… 都能够, 不过官网的标准都是放在一个页面上的, 每次关上都须要加载所有内容, 速度会十分慢.

这里举荐一个我的项目 read262. 用 read262 的话, 能够分章节浏览, 要查看某个章节, 只须要加载那个章节的内容, 当须要关上标准多个局部进行对照时会很不便. 不过 read262 会依据 https://tc39.es/ecma262 的更新主动重键, 所以只有最新的标准内容, 如果须要看其余版本的标准, 还是须要到 ECMA-262 去看对应的版本.read262能够间接应用在线版 https://read262.jedfox.com

JS 引擎

举荐一个库 engine262, 之前我说看引擎的源码会更靠近实现, 只是碍于浏览难度来说, 浏览标准会更容易一些. 其实有一个库是用 JavaScript 实现的引擎, 这样源码的浏览难度显然小了很多. 不过我举荐还是先去看标准, 而后在理论去 engine262 源码中查看对应的实现, 最初还能够将代码下载到本地运行, 理论去调试源码的运行的过程, 以印证对标准的了解.

engine262会依据最新的标准去实现, 而咱们看的有时候不肯定是最新的标准,engine262也没有根据标准的版本去做标记. 这里有一个小技巧, 能够先找到实现标准对应的源码, 而后看那个文件的提交记录, 找到跟标准批改的工夫相吻合的提交, 而后去看那个提交中的实现就跟标准中的形容统一了.

写在最初

这篇文章通过一个例子, 展现如何通过浏览标准去找到暗藏在 JavaScript 代码背地的机密. 当然如果仅仅只是得出一个论断, 其实并没有多大意义, 像例子中的状况, 属于十分边界的状况, 事实中能遇到的概率应该不大.

我心愿能通过这篇文章让大家更多的理解标准, 并且通过下面列出的材料去学习和把握如何浏览标准的技巧, 这样当咱们遇到某些问题时能够去找到最权威的材料解答纳闷. 不过标准中的大多数常识可能对于咱们日常开发都太大帮忙, 咱们只须要把握浏览的技巧, 在有须要的时候去翻翻它即可.

正文完
 0