0%

async-await原理

Generator和协程

生成器函数的具体使用方式:

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

Generator返回的是一个协程,协程是一种比线程更轻量级的存在,你可以把协程看出是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是A协程,要启动B协程,那么协程就需要把主线程的控制权交给B协程。如果从A协程启动B协程,把A协程称为B协程的父协程

一个进程拥有多个线程,一个线程也可以拥有多个协程,协程不是由操作系统内核管理,而完全是由程序控制(也就是用户态执行),好处就是性能得到了提升,不会像线程切换那样消耗资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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创建一个写成gen,创建后,gen协程并没有立即执行
  • 要让gen协程执行,需要通过调用gen.next
  • 当协程正在执行时,可以通过yield关键字来暂停gen协程的执行,并返回主信息给父协程
  • 如果协程在执行期间,遇到return关键字,那么js引擎会结束当前协程,并将return后面的内容返回给父协程

父协程有自己的调用栈,gen 协程时也有自己的调用栈,当 gen 协程通过 yield 把控制权交给父协程时,V8 是如何切换到父协程的调用栈?当父协程通过 gen.next 恢复 gen 协程时,又是如何切换 gen 协程的调用栈?

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

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

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

使用Promise和generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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函数外部代码:

  • let gen=foo()创建gen协程
  • 父协程中通过执行gen.next把主线程控制权交给gen协程
  • gen协程获取到主线程控制权,就调用fetch函数创建一个Promise对象reponse1,然后通过yield暂停gen协程的执行,将response1返回给父协程
  • 父协程恢复执行后,调用reponse1.then方法等待结果
  • 等通过fetch发起的请求完成后,会调用then中回调函数,then中的回调函数拿到结果后,通过调用gen.next放弃主线程控制权

把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器

1
2
3
4
5
6
7
8
9
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是一个通过异步执行隐式返回Promise作为结果的函数

调用async的foo函数返回一个Promise对象,

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)

foo函数被async标记,当进入该函数时,js引擎会保存当前调用栈信息,当执行到await(100),会默认创建一个Promise对象。

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

在这个promise__对象创建过程中,executor函数调用resolve函数,js引擎会将该任务提交给微任务,然后js引擎会暂停当前协程执行,将主线程的控制权交给父协程执行,同时将promise__对象返回给父协程,主线程的控制权已经交给父协程,这时候父协程要做的事就是调用promise_.then监控 promise状态的改变。继续执行父协程的流程,执行console.log(3),父协程执行结束后,在结束之前,会进入微任务检查点,执行微任务队列,微任务队列有resolve(100),触发promise_.then的回调函数

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