我的编程空间,编程开发者的网络收藏夹
学习永远不晚

JavaScript引擎如何实现async/await

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

JavaScript引擎如何实现async/await

这篇文章主要介绍了JavaScript引擎如何实现async/await,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

    前言

    我们都知道Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰

    JavaScript 引擎是如何实现 async/await 的。如果上来直接介绍 async/await 的使用方式的话,那么你可能会有点懵,所以我们就从其最底层的技术点一步步往上讲解,从而带你彻底弄清楚 async 和 await 到底是怎么工作的。

    首先介绍生成器(Generator)是如何工作的,接着讲解 Generator 的底层实现机制——协程(Coroutine);又因为 async/await 使用了 Generator 和 Promise 两种技术,所以紧接着我们就通过 Generator 和 Promise 来分析 async/await 到底是如何以同步的方式来编写异步代码的。

    生成器 VS 协程

    生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。

    function* genDemo() {    console.log("开始执行第一段")    yield 'generator 2'    console.log("开始执行第二段")    yield 'generator 2'    console.log("开始执行第三段")    yield 'generator 2'    console.log("执行结束")    return 'generator 2'}console.log('main 0')let gen = genDemo()console.log(gen.next().value)console.log('main 1')console.log(gen.next().value)console.log('main 2')console.log(gen.next().value)console.log('main 3')console.log(gen.next().value)console.log('main 4')

    执行上面这段代码,观察输出结果,你会发现函数 genDemo 并不是一次执行完的,全局代码和 genDemo 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。下面我们就来看看生成器函数的具体使用方式:

    • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。

    • 外部函数可以通过 next 方法恢复函数的执行。

    关于函数的暂停和恢复,相信你一定很好奇这其中的原理,那么接下来我们就来简单介绍下 JavaScript 引擎 V8 是如何实现一个函数的暂停和恢复的,这也会有助于你理解后面要介绍的 async/await。

    要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

    正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

    为了让你更好地理解协程是怎么执行的,我结合上面那段代码的执行过程,画出了下面的“协程执行流程图”,你可以对照着代码来分析:

    JavaScript引擎如何实现async/await

    从图中可以看出来协程的四点规则:

    • 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。

    • 要让 gen 协程执行,需要通过调用 gen.next。

    • 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。

    • 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

    不过,对于上面这段代码,你可能又有这样疑问:父协程有自己的调用栈,gen 协程时也有自己的调用栈,当 gen 协程通过 yield 把控制权交给父协程时,V8 是如何切换到父协程的调用栈?当父协程通过 gen.next 恢复 gen 协程时,又是如何切换 gen 协程的调用栈?

    要搞清楚上面的问题,你需要关注以下两点内容。

    第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。

    第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

    为了直观理解父协程和 gen 协程是如何切换调用栈的

    JavaScript引擎如何实现async/await

    到这里相信你已经弄清楚了协程是怎么工作的,其实在 JavaScript 中,生成器就是协程的一种实现方式,这样相信你也就理解什么是生成器了。那么接下来,我们使用生成器和 Promise 来改造开头的那段 Promise 代码。改造后的代码如下所示:

    //foo函数function* foo() {    let response1 = yield fetch('https://www.geekbang.org')    console.log('response1')    console.log(response1)    let response2 = yield fetch('https://www.geekbang.org/test')    console.log('response2')    console.log(response2)}//执行foo函数的代码let gen = foo()function getGenPromise(gen) {    return gen.next().value}getGenPromise(gen).then((response) => {    console.log('response1')    console.log(response)    return getGenPromise(gen)}).then((response) => {    console.log('response2')    console.log(response)})

    从图中可以看到,foo 函数是一个生成器函数,在 foo 函数里面实现了用同步代码形式来实现异步操作;但是在 foo 函数外部,我们还需要写一段执行 foo 函数的代码,如上述代码的后半部分所示,那下面我们就来分析下这段代码是如何工作的。

    • 首先执行的是let gen = foo(),创建了 gen 协程。然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。

    • gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。

    • 父协程恢复执行后,调用 response1.then 方法等待请求结果。

    • 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。

    以上就是协程和 Promise 相互配合执行的一个大致流程。不过通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器(可参考著名的 co 框架),如下面这种方式:

    function* foo() {    let response1 = yield fetch('https://www.geekbang.org')    console.log('response1')    console.log(response1)    let response2 = yield fetch('https://www.geekbang.org/test')    console.log('response2')    console.log(response2)}co(foo());

    通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

    async/await

    虽然生成器已经能很好地满足我们的需求了,但是程序员的追求是无止境的,这不又在 ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。其实 async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。要搞清楚 async 和 await 的工作原理,我们就得对 async 和 await 分开分析。

    async

    我们先来看看 async 到底是什么?根据 MDN 定义,async 是一个通过异步执行隐式返回 Promise 作为结果的函数。

    这里我们先来看看是如何隐式返回 Promise 的,你可以参考下面的代码:

    async function foo() {    return 2}console.log(foo())  // Promise {<resolved>: 2}

    执行这段代码,我们可以看到调用 async 声明的 foo 函数返回了一个 Promise 对象,状态是 resolved,返回结果如下所示:

    Promise {<resolved>: 2}

    await

    我们知道了 async 函数返回的是一个 Promise 对象,那下面我们再结合文中这段代码来看看 await 到底是什么。

    async function foo() {    console.log(1)    let a = await 100    console.log(a)    console.log(2)}console.log(0)foo()console.log(3)

    观察上面这段代码,你能判断出打印出来的内容是什么吗?这得先来分析 async 结合 await 到底会发生什么。在详细介绍之前,我们先站在协程的视角来看看这段代码的整体执行流程图:

    JavaScript引擎如何实现async/await

    结合上图,我们来一起分析下 async/await 的执行流程。

    首先,执行console.log(0)这个语句,打印出来 0。

    紧接着就是执行 foo 函数,由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,JavaScript 引擎会保存当前的调用栈等信息,然后执行 foo 函数中的console.log(1)语句,并打印出 1。

    接下来就执行到 foo 函数中的await 100这个语句了,这里是我们分析的重点,因为在执行await 100这个语句时,JavaScript 引擎在背后为我们默默做了太多的事情,那么下面我们就把这个语句拆开,来看看 JavaScript 到底都做了哪些事情。

    当执行到await 100时,会默认创建一个 Promise 对象,代码如下所示

    let promise_ = new Promise((resolve,reject){  resolve(100)})

    在这个 promise_ 对象创建的过程中,我们可以看到在 executor 函数中调用了 resolve 函数,JavaScript 引擎会将该任务提交给微任务队列。

    然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。

    主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来 3。

    随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数,如下所示:

    promise_.then((value)=>{   //回调函数被激活后  //将主线程控制权交给foo协程,并将vaule值传给协程})

    该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。

    foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

    以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。

    感谢你能够认真阅读完这篇文章,希望小编分享的“JavaScript引擎如何实现async/await”这篇文章对大家有帮助,同时也希望大家多多支持编程网,关注编程网行业资讯频道,更多相关知识等着你来学习!

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    JavaScript引擎如何实现async/await

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档

    猜你喜欢

    JavaScript引擎如何实现async/await

    这篇文章主要介绍了JavaScript引擎如何实现async/await,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。前言我们都知道Promise 能很好地解决回调地狱的问题
    2023-06-29

    JavaScript如何优雅的处理async/await

    这篇文章给大家分享的是有关JavaScript如何优雅的处理async/await的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。优雅的处理 async/await使用方法:无需每次使用 async/await 都
    2023-06-27

    JavaScript怎么实现碰撞物理引擎

    本文小编为大家详细介绍“JavaScript怎么实现碰撞物理引擎”,内容详细,步骤清晰,细节处理妥当,希望这篇“JavaScript怎么实现碰撞物理引擎”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。效果图:接下来
    2023-06-27

    如何使用Java实现语音引擎

    这篇文章主要为大家展示了“如何使用Java实现语音引擎”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用Java实现语音引擎”这篇文章吧。一、试用语音引擎 要使用这个语音引擎,你必须在CLA
    2023-06-03

    V8引擎如何执行JavaScript代码

    V8引擎如何执行JavaScript代码,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。题目中说到的V8引擎,大家自然会联想到Node.js。我们先看一下官方对Node.js的定
    2023-06-16

    Spring Boot整合Elasticsearch如何实现全文搜索引擎

    这篇文章给大家分享的是有关Spring Boot整合Elasticsearch如何实现全文搜索引擎的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。简单说,ElasticSearch(简称 ES)是搜索引擎,是结构化
    2023-05-30

    Java SpringBoot @Async如何实现异步任务

    本篇内容介绍了“Java SpringBoot @Async如何实现异步任务”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!依赖pom.xml
    2023-07-04

    python基于搜索引擎如何实现文章查重功能

    这篇文章给大家分享的是有关python基于搜索引擎如何实现文章查重功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Python主要用来做什么Python主要应用于:1、Web开发;2、数据科学研究;3、网络爬虫
    2023-06-14

    vue3+async-validator如何实现表单验证

    本篇内容主要讲解“vue3+async-validator如何实现表单验证”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“vue3+async-validator如何实现表单验证”吧!搭建vue3
    2023-07-02

    编程热搜

    • Python 学习之路 - Python
      一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
      Python 学习之路 - Python
    • chatgpt的中文全称是什么
      chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
      chatgpt的中文全称是什么
    • C/C++中extern函数使用详解
    • C/C++可变参数的使用
      可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
      C/C++可变参数的使用
    • css样式文件该放在哪里
    • php中数组下标必须是连续的吗
    • Python 3 教程
      Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
      Python 3 教程
    • Python pip包管理
      一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
      Python pip包管理
    • ubuntu如何重新编译内核
    • 改善Java代码之慎用java动态编译

    目录