• 深入浅出JavaScript异步编程


      随着移动互联网基础网速的飞速提升和各种设备硬件的革命性升级,人们对web应用功能的期待越来越高,浏览器性能因浏览器内核的革命性升级得到飞速提升,受浏览器性能制约的前端技术也迎来飞速发展。正如Atwood定律所言:“凡是可以用 JavaScript 来写的应用,最终都会用 JavaScript 来写。”的确,现在的前端技术涉足领域广泛,有web应用开发、服务端开发、PC桌面程序开发、移动APP开发、IDE开发、CLI工具开发及工程化流程工具开发等。但随着前端技术日新月异的发展,JavaScript中的异步编程弊病问题也越来越明显地暴露出来,异步编程问题的解决方案也在快速的迭代优化。

      本文将为大家解答以下疑问:什么是异步编程?为什么浏览器下会有异步编程?异步回调有哪些问题?如何解决异步回调问题?浏览器支撑的新方案的原理?

      1.什么是异步编程

      异步和同步对应,异步编程即处理异步逻辑的代码,JavaScript中最原始的就是使用回调函数。所以,我们只要理清同步回调和异步回调的区别,就可以理解什么是异步编程了。

      请先看同步回调示例:

      

    在这里插入图片描述

      执行顺序2、1、3,先输出1后输出3,可见,同步回调:回调函数callback是在主函数dowork返回之前执行的。

      再看异步回调示例:

      

    在这里插入图片描述

      先输出3后输出1,可见,异步回调:回调函数并没有在主函数内部被调用,而是在主函数外部执行,主函数返回后才执行。

      2. 为什么浏览器下有异步编程

      Chrome下的异步编程模型,如下图:

      

    在这里插入图片描述

      浏览器渲染进程中的渲染流水线主线程是单线程的,主线程发起耗时任务,交给其他进程执行,等处理完后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,触发相关的回调操作,并将任务交给另一个进程去处理,这时页面主线程会继续执行消息队列中的任务。

      浏览器设计时,最初选择了单线程架构,结合事件循环和消息队列的实现方式,我们在JavaScript开发中,也会经常遇到异步回调。

      而异步回调,影响了我们的编码方式,我们必须直面异步回调中的一些问题。

      3. 异步回调有什么问题

      如果我们一直选择使用异步回调编写代码,当面临复杂的应用需求,如遇到有依赖关系的异步逻辑或者发送ajax请求时,则会较为麻烦。

      看个示例:

      

    在这里插入图片描述

      这段代码可以正常执行,但是里面却执行了5次回调。

      这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的常规思维,也即异步回调影响到了我们的编码方式。

      遇到这种情况,我们通常可以封装异步代码,降低处理异步回调次数,让处理流程变得线性,如jQuery的$.ajax就是这么做的。

      这样做,虽然在一些简单的场景下运行效果也非常好,但遇到非常复杂的场景时,嵌套了太多的回调函数就很容易使自己陷入回调地狱。

      比如:

      

    在这里插入图片描述

      这是一个典型的多层嵌套ajax请求的场景,这时回调地狱问题就暴露无疑了,因为这段代码逻辑不连续,让人感到凌乱。

      此时,总结异步回调问题,如下:

      嵌套调用,层层嵌套,层次多了代码可读性差了。

      任务的不确定性,如上方ajax请求,总会有成功或者失败,每一层的任务都有判断逻辑和错误处理逻辑,这样就让代码更加混乱了。

      4. 解决异步回调问题的方案

      想解决异步编程问题,要考虑的是:一是消灭回调,二是合并错误判断和处理。

      目前较好的解决方案有:Promise和Async/await

      Promise示例:

      

    在这里插入图片描述

      代码清晰了,Promise 使用回调函数延迟绑定解决了回调函数嵌套的问题,如p1.then,p2.then等,这便是同步编码的风格了。

      Promise的回调函数返回值有穿透到最外层的性质,具体到错误处理的场景,就是说对象的错误具有“冒泡”的性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止,这样就把错误判断和处理逻辑合并了。

      Promise方案,虽然实现了同步风格编程,但是里面包含了大量的then函数,让代码还是不太容易阅读。

      以下为Async/await示例:

      

    在这里插入图片描述

      我们想要输出2以后再输出3,虽然xs函数是异步的,但是我们的写法是同步的,代码逻辑是连续的,这样代码就更加清晰可读了。

      5. 从浏览器原理分析Promise原理

      Promise是V8引擎提供的,所以暂时看不到 Promise 构造函数的细节。V8 在Promise 中使用微任务,来实现回调函数的延迟绑定。

      微任务是V8提供的,当前宏任务执行的时候,V8会为其创建一个全局执行上下文,V8引擎也会在内部创建一个微任务队列,宏任务执行过程中产生的微任务都会放入微任务队列。

      当前宏任务中的 JavaScript 快执行完成时,也即在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

      如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。

      浏览器执行宏任务、微任务和渲染的循环顺序是,宏任务、该宏任务的微任务队列、渲染,再执行消息队列中下个宏任务、该宏任务的微任务队列、渲染,如此循环执行。

      综上可知,从本质和浏览器原理来说, js实现异步回调的方式可以有两种:

      把回调函数添加到(消息队列)宏任务队列内,当执行完当前宏任务和它的微任务队列后,等合适的时机或者可执行代码容器空闲时执行。如setTimeout延迟任务和ajax异步请求任务。

      把回调函数添加到当前宏任务的微任务队列,等待当前宏任务执行结束前,依次执行。

      我们猜测模拟实现个Promise,说明为何要用微任务。

      

    在这里插入图片描述

      这里,我们没有用异步回调,而是同步回调,但是回调函数还是延迟绑定,这样执行时就会报错,因为我们同步调用回调时,回调函数还没绑定。

      如果此时resolve改为使用宏任务队列的异步回调setTimeout,虽然可以实现功能,但是执行回调的时机会被延迟,代码执行效率则被降低。

      

    在这里插入图片描述

      所以,v8采用微任务实现promise,是为了在方便开发与执行效之间寻找到一个完美的平衡。

      6. 生成器与协程

      生成器Generator是v8提供的,生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。底层实现机制是协程(Coroutine)。

      看个生成器的例子:

      

    在这里插入图片描述

      执行结果为:

      

    在这里插入图片描述

      执行生成器函数,并不执行函数内代码,而是返回一个对象引用,可赋值给外部函数的变量。外部函数通过变量对象的next 方法开始执行生成器函数的内部代码;在生成器函数内部执行一段代码时,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该生成器函数的执行;外部函数通过next().value获得生成器函数的返回值。外部函数可以通过 next 方法再次恢复生成器函数的执行。以此类推执行。

      没有 yield时,遇到return时,也暂停生成器函数的执行,这里应该说是回收调用栈,结束函数更准确,而不是暂停。

      V8 是如何实现一个函数的暂停和恢复的?

      这里涉及到协程的概念,协程比线程更轻量,协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在一个线程上同时只能执行一个协程。但是,协程不是被操作系统内核所管理的,而完全是由程序所控制。这样,性能就有了很大的提升,不会像线程切换那样消耗资源。

      yield 和 .next切换生成器函数的暂停和恢复,其实就是在关闭和开启生成器函数对应的子协程,子协程和父协程在主线程上交互执行,并非并发执行的。在切换父子协程时,关闭前都会先保存当前协程的调用栈信息,以便再次开启时,继续执行。所以,从浏览器角度看,生成器的底层实现是协程。

      7. co框架的原理,Promise与生成器的结合

      生成器函数可以理解成一个异步操作的容器,它装着一些异步操作,但并不会在实例化后立即执行。而co的思想是在恰当的时候执行这些异步操作。在一个异步操作执行完毕以后通知下一个异步操作开始执行,需要依靠回调函数或者promise来实现。所以,co要求生成器函数里yield的是thunk(回调机制)或者promise。

      我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器。co框架就是个执行器。

      promise结合生成器函数的实现示例:

      

    在这里插入图片描述

      run2是执行器,也是co框架的源码里面的promise回调机制实现的原理。

      8. 从协程和微任务看Async/await

      async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

      

    在这里插入图片描述

      可见async 执行完,v8让它返回的是一个Promise。

      Async/await在一起会发生什么?

      

    在这里插入图片描述

      先执行3,后输出a和2,这就体现了异步。

      上面的await "我会被放入await返回proimse的executor内"会被v8处理为:

      

    在这里插入图片描述

      可见,其实await 代码,会默认返回promise,如果await后面的是个值,值会直接作为resolve函数的参数内容并调用resolve,返回promise;如果在await后面加个函数,则需要返回promise,如接个async function(){},这就对应了前文,async函数执行默认返回了promise。

      同时,把后续代码,作为await 返回promise的回调函数延迟绑定了,因为使用了微任务实现了延迟绑定,所以回调也就是后续代码被放到了微任务队列,所以会异步执行。

      我们从协程角度,分析上面代码的执行原理。

      调用hai函数,开启hai函数的子协程;

      执行输出1;

      遇到await,把后续代码加入promise的回调函数,其实进入了微任务队列。同时,把resolve函数结果值返回给a;

      这时,暂停子协程,控制权给主线程;

      主线程执行输出3;

      主线程执行结束前,查看微任务队列,发现有微任务,也就是上面加入的,执行微任务;

      执行微任务,立马恢复子协程,执行输出a和2。

      执行完毕,关闭子协程,控制权交给主线程。

      所以,才有了上面的执行结果。

      综合分析async/await:

      郑州专业人流医院http://www.hnzzxb.com/

      

    在这里插入图片描述

      输出顺序是:

      

    在这里插入图片描述

      这里关键点是:郑州割包皮多少钱http://www.zztjxb.com/

      4是在主线程上,属于宏任务内,按顺序先执行;

      2、1都是在字协程内执行,其中2所在的协程是1所在协程的父协程,但是都是在当前宏任务阶段执行;这里涉及了主线程、父协程、子协程的关闭交互。

      bar 内的await把3加入了微任务队列,所以在当前宏任务执行完后才执行;

      6和8 是在主线程上,属于宏任务内,按顺序执行,7在6所在的Promise内的延迟回调内,这时加入了微任务队列,比3加入的晚,所以7晚于3。

      执行3时,处于微任务阶段,开启了子协程;

      执行7时,处于微任务阶段,又关闭了子协程,控制权在主线程。

      5是延迟函数,延迟任务,属于下一个宏任务,所以会在当前微任务执行完,才执行写个宏任务。

  • 相关阅读:
    kubernetes 之配置问题
    awk 输入文件包含某个关键字的整行记录或者某个字段
    Kubernetes资源在线生成工具
    使用print计算一串字符串各字符出现的个数
    Kubernetes 部署可视化dashboard管理面板
    nexus 3.37.302安装
    阿里云免费证书验证,nginx返回403,/.wellknown/pkivalidation/
    通过docker安装openldap并通过ldapadmin配置用户与第三方软件集成ldap配置
    kubernetes创建storageclass
    k8s快速生成一个deploymenet的yaml文件
  • 原文地址:https://www.cnblogs.com/sushine1/p/14005775.html
Copyright © 2020-2023  润新知