浏览器原理 19 # JavaScript 引擎是如何实现 async / await 以同步的方式来编写异步代码的?

简介: 浏览器原理 19 # JavaScript 引擎是如何实现 async / await 以同步的方式来编写异步代码的?

说明

浏览器工作原理与实践专栏学习笔记



为什么引入 async / await


先来看一个使用 fetch 来发起对远程资源的请求:


20210426093114144.png


fetch 请求例子

fetch('https://www.geekbang.org')
  .then((response) => {
      console.log(response)
      return fetch('https://www.geekbang.org/test')
  }).then((response) => {
      console.log(response)
  }).catch((error) => {
      console.log(error)
})


控制台效果:

20210426093156332.png


从这段 Promise 代码可以看出来,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。基于这个原因,ES7 引入了 async / await,它提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。


用 async / await 改造代码


async function foo(){
  try{
    let response1 = await fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
  }catch(err) {
       console.error(err)
  }
}
foo()


生成器 VS 协程


为什么突然又讲这个东东?

因为 async / await 使用了 GeneratorPromise 两种技术,而 Generator 的底层实现机制就是协程(Coroutine)


生成器函数

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

function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 1'
    console.log("开始执行第二段")
    yield 'generator 2'
    console.log("开始执行第三段")
    yield 'generator 3'
    console.log("执行结束")
    return 'generator 4'
}
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')


执行结果如下:

20210426101231562.png


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


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


那么 JavaScript 引擎 V8 是如何实现一个函数的暂停和恢复?


要搞懂这个问题就要了解协程的概念



协程


协程是一种比线程更加轻量级的存在。


   可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。


怎么理解只能执行一个协程?


比如:当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。


协程执行流程图:

20210426102759460.png


协程的四点规则:

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

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

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

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


V8 是如何切换到父协程以及 gen 协程的调用栈?


   当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。


   当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。


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


gen 协程和父协程之间的切换:

20210426103628875.png


在 JavaScript 中,生成器就是协程的一种实现方式

使用生成器和 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)
})


首先执行的是 let gen = foo(),创建了 gen 协程。


然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。


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


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


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




执行器


把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器(可参考著名的 co 框架

关于 co 框架可以参考: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

async / await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。


async

MDN:async 函数

根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数



隐式返回 Promise

async function foo() {
    return 2
}
console.log(foo())


20210426113048237.png


await

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


执行结果:

20210426114324444.png

执行流程图:

20210426115329171.png


promise_.then 中的回调函数

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





目录
相关文章
|
11天前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11天前
|
JavaScript 前端开发 API
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11天前
|
移动开发 运维 供应链
通过array.some()实现权限检查、表单验证、库存管理、内容审查和数据处理;js数组元素检查的方法,some()的使用详解,array.some与array.every的区别(附实际应用代码)
array.some()可以用来权限检查、表单验证、库存管理、内容审查和数据处理等数据校验工作,核心在于利用其短路机制,速度更快,节约性能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11天前
|
供应链 JavaScript 前端开发
通过array.every()实现数据验证、权限检查和一致性检查;js数组元素检查的方法,every()的使用详解,array.some与array.every的区别(附实际应用代码)
array.every()可以用来数据验证、权限检查、一致性检查等数据校验工作,核心在于利用其短路机制,速度更快,节约性能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11天前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
前端开发 JavaScript
8 种技巧让你编写更简洁的 JavaScript 代码
8 种技巧让你编写更简洁的 JavaScript 代码
279 0
8 种技巧让你编写更简洁的 JavaScript 代码
|
Web App开发 JavaScript 前端开发
|
Web App开发 JavaScript 前端开发
【译】如何编写避免垃圾开销的实时 JavaScript 代码
本文讲的是【译】如何编写避免垃圾开销的实时 JavaScript 代码,哇,这篇文章已经写了有很长一段时间了,十分感谢那些精彩的回复!其中有一些对于一些技术的指正,如使用 'delete' 。我知道了使用它可能会导致其他的降速问题,因此,我们在引擎中极少使用它。
1178 0