上一篇文章中我们Promise的相关知识,很好的处理了回调的问题,但是也充满了then方法,如果处理流程很复杂,那么then就会很多,也会显得不太友好
现在有这样一个场景,先请求一个资源,得到结果之后,再去请求 另外一个资源,fetch返回的是一个Promise对象,是浏览器原生支持的,并没有使用XMLHttpRequest来封装
fetch('https://www.example1.org')
.then((response) => {
console.log(response)
return fetch('https://www.example2.org/test')
}).then((response) => {
console.log(response)
}).catch((error) => {
console.log(error)
})
ES7中引入了 async/await 函数, 提供了在不阻塞主线程的情况下使用同步代码来实现异步访问资源的能力,并且使代码逻辑更清晰,参考下面的代码:
async function foo(){
try{
let response1 = await fetch('https://www.example1.org')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://www.example2.org/test')
console.log('response2')
console.log(response2)
}catch(err) {
console.error(err)
}
}
foo()
整个异步处理的逻辑都是使用同步代码的方式来实现的,而且还支持 try catch 来捕获异常,这就是完全在写同步代码,所以是非常符合人的线性思维的,下面来深入分析一下async / await的实现原理
生成器和协程
生成器 函数是一个带星号的函数,可以暂停执行和恢复执行,参考下面代码:
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 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。下面我们就来看看生成器函数的具体使用方式:
1、在生成器函数内部执行一段代码,如果遇到yield关键字,那么JavaScript引擎将返回yield后面的关键字给外部,并暂停函数的执行
2、外部函数可以通过next方法恢复函数的执行
为什么生成器函数可以实现暂停和恢复呢?这要从V8的内部实现来解释。
协程
协程比线程更轻量,可以看出是跑在线程上的任务,一个线程上有多个协程,一个时间只能执行一个协程,假如当前执行协程A,要启动协程B,那么A就需要将主线程的控制权交给B,也就是A暂停,B执行。如果从A协程启动B协程,那么A协程就称为B协程的父协程
一个进程可以拥有多个线程,一个线程可以有多个协程,协程不是被操作系统内核所管理,而是完成由程序来控制,来一起看一下协程执行的示意图:
协程的调用栈是如何切换的呢?gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
生成器就是协程的一种实现,理解了协程的工作原理,也就搞清楚了生成器函数,下面我们使用生成器和Promise来改造开始的那一段代码:
//foo函数
function* foo() {
let response1 = yield fetch('https://www.example1.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.example2.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函数是一个生成器,首先创建一个协程gen,然后在父协程里通过gen…next()把主协程的控制权交给gen协程,gen协程调用fetch生成一个promise对象,然后暂停子协程,将结果response1返回给外部,父协程通过then拿到response1的结果,依次往下执行
async / await
使用async和await可以使我们告别执行器和和生成器,更加直观简洁。
1、async
async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。来看下面这段简单的代码:
async function foo() {
return 2
}
console.log(foo()) // Promise {<resolved>: 2}
执行这段代码,并不能打印出我们想要的值,返回一个promise对象,状态是resolved。
Promise {<fulfilled>: 2}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 2
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的执行流程
首先,执行console.log(0)
紧接着执行foo函数,由于foo函数是被async标记过,所以当进入该函数的时候,JavaScript引擎会保存当前调用栈等信息,然后执行foo函数的console.log(1)
然后遇到了await 100, 我们一起来看一下await到底做了什么,执行await 100,会默认创建一个promise对象,代码如下:
let promise_ = new Promise((resolve,reject){
resolve(100)
})
在这个promise_对象创建的过程中,我们看到执行函数executor函数调用了resolve函数,JavaScript引擎会将该任务交给微任务队列
然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权交给父协程,同时会将promise_对象返回给父协程
主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用promise_.then来监控promise的状态改变
接下来继续执行父协程的流程,console.log(3), 然后父协程执行结束,结束之前进入微任务检查点,执行微任务队列,微任务中有resolve(100)等待执行,执行到这,会触发promise_.then中的回调函数:
promise_.then((value)=>{
//回调函数被激活后
//将主线程控制权交给foo协程,并将vaule值传给协程
})
该回调函数被激活以后,会将主线程的控制权交给foo协程,同时将value值传出去
foo协程激活之后,会吧刚才的value付给变量a,然后foo协程继续执行后续语句,执行完成之后,将控制权交给父协程