0
点赞
收藏
分享

微信扫一扫

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


说明

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

为什么引入 async / await

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

  • ​​阮一峰的网络日志:Fetch API 教程​​
  • ​​MDN:Fetch API​​

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

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)
})

控制台效果:

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

从这段 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​​​ 使用了 ​​Generator​​​ 和 ​​Promise​​​ 两种技术,而 ​​Generator​​​ 的底层实现机制就是​​协程(Coroutine)​​。

  • ​​MDN:function*​​

生成器函数

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

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')

执行结果如下:

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

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

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

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

协程

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

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

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

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

协程执行流程图

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

协程的四点规则:

  1. 通过调用生成器函数​​genDemo​​​ 来创建一个协程​​gen​​​,创建之后,​​gen​​ 协程并没有立即执行。
  2. 要让 gen 协程执行,需要通过调用​​gen.next​​。
  3. 当协程正在执行的时候,可以通过​​yield​​​ 关键字来暂停​​gen​​ 协程的执行,并返回主要信息给父协程。
  4. 如果协程在执行期间,遇到了​​return​​​ 关键字,那么​​JavaScript​​​ 引擎会结束当前协程,并将​​return​​ 后面的内容返回给父协程。

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

  1. 当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。
  2. 当在父协程中执行​​gen.next​​ 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
  3. gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和​​gen.next​​ 来配合完成的。

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

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

在 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)
})

  1. 首先执行的是​​let gen = foo()​​,创建了 gen 协程。
  2. 然后在父协程中通过执行​​gen.next​​ 把主线程的控制权交给 gen 协程。
  3. gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象​​response1​​​,然后通过 yield 暂停 gen 协程的执行,并将​​response1​​ 返回给父协程。
  4. 父协程恢复执行后,调用​​response1.then​​ 方法等待请求结果。
  5. 等通过 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())

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

await

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

执行结果:

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

执行流程图:

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

​promise_.then​​ 中的回调函数

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


举报

相关推荐

0 条评论