从生成器和协程的角度详解async和await,图文解析

Promise通过使用微任务实现延迟绑定执行回调函数解决了代码回调地狱的问题,并使代码线性化表述。随之而来的问题是会有大量的then函数,如果真的像是正常书写代码的形式,要怎么做呢?

async/await

ES7引入了async/await关键字,和setTimeout同为异步任务。不阻塞主线程的前提下,通过同步代码书写的方式获取异步资源。如下例所示:

js 复制代码
async function testFn() {
    try {
        let res1 = await getToken()
        console.log(res1, "res1")

        let res2 = await getUserInfo(res1.token)
        console.log(res2, "res2")
    } catch (error) {
        console.log(error)
    }
}

上述代码首先通过getToken接口从后端获取到最新的token,在通过token作为getUserInfo的入参获取到用户信息。

如果像没有Promise和async/await时,我们肯定是在getToken的接口回调里面再套一个getUserInfo接口,同时要分别写两次错误的异常处理。ES7引入了async/await之后,我们可以使用同步的代码书写方式来代替异步的处理逻辑。那么async/await是怎么实现的呢?

生成器

async/await使用的技术之一就是生成器。Generator(生成器)是一类特殊的函数,跟普通函数声明时的区别是加了一个*号。生成器函数可以暂停和恢复执行。如下例所示:

js 复制代码
function* genFn() {
    console.log("genFn first part")
    yield 'genFn-part1'

    console.log("genFn two part")
    yield 'genFn-part2'

    console.log("genFn third part")
    yield 'genFn-part3'

    console.log("genFn finish")
    return 'genFn-finish'
}

console.log("script start")
const gf = genFn()
console.log(gf.next())
console.log("script-1")
console.log(gf.next())
console.log("script-2")
console.log(gf.next())
console.log("script-3")
console.log(gf.next())
console.log("script-4")

你可以先执行一下看看结果。

  • 生成器函数genFn在第16行赋值给gf,不同于普通函数的是,genFn函数初始为暂停状态,不会立即执行;
  • 每次调用next函数会打破暂停状态向下执行,直到碰到yield,返回yield后面的内容后,再次进入暂停状态;
  • 或者碰到return,返回return后面的内容后,结束函数执行。

具体执行结果如下图所示:

通过上图我们可以看到全局代码和生成器函数genFn交替执行。生成器内部初始为暂停状态,遇到yield后为暂停状态。通过外部调用next方法恢复生成器函数的执行。那这个暂停和恢复是怎么实现的呢?我们来看一下协程的概念

协程

协程是什么呢?协程是用户态的线程

  • 协程由程序控制,不同于线程由操作系统内核控制,上下文创建和切换需要消耗CPU资源,多个任务切换的性能得到了提升;
  • 协程是比线程更轻量化的存在,可以看做是跑在线程的上的任务;
  • 一个线程可以存在多个协程,但一个线程同时只能执行一个协程;
  • 协程之间的切换是函数执行权的转移;
  • 创建协程时,会从进程的堆中分配一段内存作为协程的栈,只有kb大小。线程有8MB

那上段代码中协程是怎么执行和切换的呢?

具体执行如上图所示。

  1. 通过getFn()创建gf协程。协程处于暂停状态。
  2. 父协程通过调用gf.next(),把主线程的控制权交给gf协程。
  3. gf协程执行一部分后,通过yield关键字暂停协程执行,返回信息给父协程,并将主线程控制权交给父协程。
  4. gf协程遇到return关键字,js引擎结束当前协程,返回信息给父协程。

代码中父协程和子协程交替执行,通过yield和gf.next来交换主线程的控制权。其中在gf协程中调用yield方法时,js引擎会保存gf协程当前的调用栈,并恢复父协程的调用栈信息。在父协程执行gf.next方法时,js引擎操作翻转。

代码改造

js 复制代码
async function testFn() {
    try {
        let res1 = await getToken()
        console.log(res1, "res1")

        let res2 = await getUserInfo(res1.token)
        console.log(res2, "res2")
    } catch (error) {
        console.log(error)
    }
}

本文开头的这段代码如果使用生成器实现的话,要怎么实现呢?这里接口修改一下,不影响整体实现

js 复制代码
function* testFn() {
    let res1 = getGoodsList({ pageSize: 10, pageNum: 1 })
    yield res1
    console.log(res1, "res1")

    let res2 = getGoodsList({ pageSize: 10, pageNum: 1 })
    yield res2
    console.log(res2, "res2")
}

const tf = testFn()
function getTestFnPromise() {
    return tf.next().value
}

getTestFnPromise()
    .then(res => {
        console.log(res, "r1")
        return getTestFnPromise()
    })
    .then(res => {
        console.log(res, "r2")
        getTestFnPromise()
    })
    .catch(err => {
        console.log(err)
    })

这时我们看到输出的结果为:

程序的执行过程应该也比较熟悉了:

  1. 声明testFn生成器函数,通过同步的形式实现异步操作;
  2. 通过testFn(),创建tf协程;
  3. 父协程通过调用getTestFnPromise函数,执行tf.next把主线程控制权交给tf协程;
  4. tf协程获取主线程控制权后,调用接口返回Promise对象,通过yield暂停tf协程执行,并将res1返回给父协程;
  5. 父协程获取主线程控制权后,调用res1.then等待执行;
  6. 接下来执行继续3-5。

以上为Promise和协程配合执行。

再看async/await

async/await使用的技术就是生成器和Promise,再往底层就是微任务和协程。目前实现还有co函数库的加持。接下来分开看一下这两个关键字。

async

根据MDN定义,async声明的函数,每次调用异步函数时,都会返回一个新的Promise对象。该对象可能是异步函数的resolve值,或者为异步函数未捕获的异常的reject值。

参考下例:

js 复制代码
async function testAsyncRes(){
    return '222'
}

async function testAsyncRes1(){
    console.log(123)
}

console.log(testAsyncRes())
console.log(testAsyncRes1())

输出结果为:

await

根据MDN定义,async声明的函数,每次调用异步函数中可以包含零个或者多个 await 表达式。await 表达式通过暂停执行使返回 promise 的函数表现得像同步函数一样,直到返回的 promise 被resolve或reject。

参考下例:

js 复制代码
async function testAwait() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log("start")
testAwait()
console.log("end")

这个输出的顺序是什么呢?我们先从协程的角度看一下执行过程:

  1. 父协程启动,执行同步代码console.log("start")
  2. JS引擎创建testAwait子协程,将主线程控制权交给testAwait协程
  3. testAwait协程执行console.log(1)
  4. 执行到await 100时,js引擎会创建Promise对象:Promise.resolve(100),将该任务添加到微任务队列。并将后续的两个输出包装成微任务
  5. JS引擎将会暂停testAwait协程执行,将控制权交给父协程
  6. 执行同步代码console.log("end")
  7. 父协程即将结束,在结束之前,会进入微任务的检查点,去执行微任务队列
  8. 检查微任务队列,发现testAwait协程暂停时注册的微任务,将控制权交给testAwait协程输出a和2。此时Promise.resolve(100)的作用就是协程恢复的触发器,解决值100为协程恢复的输入值。(这里和原文章的思路不同,原文章认为此Promise会返回给到父协程,有想法的欢迎评论区交流)

同样,各协程切换前,会维护各自的调用栈,保存当前执行位置、变量状态和函数调用上下文。

参考文献:blog.csdn.net/weixin_5648...

(12 封私信) JavaScript中的协程 - 知乎

浏览器工作原理与实践

相关推荐
小小小小宇3 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖3 小时前
http的缓存问题
前端·javascript·http
小小小小宇4 小时前
请求竞态问题统一封装
前端
loriloy4 小时前
前端资源帖
前端
源码超级联盟4 小时前
display的block和inline-block有什么区别
前端
GISer_Jing4 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂4 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端4 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端
葡萄糖o_o4 小时前
ResizeObserver的错误
前端·javascript·html
AntBlack4 小时前
Python : AI 太牛了 ,撸了两个 Markdown 阅读器 ,谈谈使用感受
前端·人工智能·后端